分代收集器
新生代回收器
Serial:复制算法 | 单线程 | 适合内存不大的场景
ParNew:复制算法 | 多线程 | Serial收集器多线程版本
Parallel Scavenge:复制算法 | 多线程 | 类ParNew,更关注吞吐量
老年代回收器
Serial Old:标记整理算法 | 单线程
Parallel Old:标记整理算法 | 多线程 | Parallel Scavenge收集器的老年代版本
CMS:标记清除算法 | 多线程 | 尽可能的缩短垃圾收集时用户线程停止时,关注延迟
执行阶段:
- 初始标记(STW):只标记GC Roots的直接关联的对象,因为只有一层对象所以STW时间很短。
- 并发标记:从GC Roots对象直接关联的对象开始遍历整个对象图,与用户线程并发执行。
- 重新标记(STW):由于并发标记是与用户线程并发执行的,那么要修正并发标记期间的已被标记但是又被用户修改的这部分对象。相比初始标记阶段,STW时间会相对长一些。
- 并发清除:因为应用的标记清除算法,可以并发执行清理的操作。(若是标记清理算法,会涉及对象的位置迁移,那么并发执行会出现问题)
缺点:
- 内存碎片:因为应用的是标记清除算法
- 无法处理浮动垃圾:浮动垃圾就是并发标记阶段原可被关联的引用链上的对象被用户线程释放(如obj = null),导致原本不需回收的对象变成了垃圾对象,这些对象称为浮动垃圾,需要在下一次GC时清理
- Concurrent Mode Failure:当并发清除阶段时老年代内存不足,导致无法存储新的对象,那么就会发生Concurrent Mode Failure。会导致CMS退化为Serial Old,停止所有的用户线程,进行一次完整的单线程垃圾回收。
不分代收集器
G1:标记复制算法 | 多线程 | 适合内存较大,低延迟的场景。Jdk1.9的默认GC。将堆内存划分为定量的Region,GC时预估每个Region的可回收价值(可以回收对象的大小与回收等评判条件),尽量把垃圾回收造成的程序卡顿STW控制在指定的时间范围内,在有限的时间内回收更多的对象。
G1垃圾回收器下的堆内存模型
-
Humongous Region:对象的大小超过了一个Region大小的50%,即认为是大对象,会被存放在humongous region中。若对象过大,可能会跨Region存储。在Mixed GC时,也会回收大对象Region。
-
逻辑分代,物理分区,Region数量最多为2048个
Minor GC(新生代回收)
- 新生代回收:Eden、Survivor Region
- 触发条件:新生代的Region(Eden、Survivor Region)占有60%的堆内存。
- 应用算法:标记复制,会STW。
- 新生代对象晋升老年代的条件:
- 对象年龄超过15。
- 存活对象大小超过60%的Survivor,会将age最大的对象晋升到老年代,即使age并没有超过15。
Mixed GC(混合回收)
- 混合回收:新生代+部分老年代+大对象(Eden、Survivor、Old、Humongous Region)一起混合回收。
- 触发条件:老年代的Region(Old Region)占有45%的堆内存。
- 回收算法:标记复制,会SWT。
- 回收执行阶段(类似CMS):
- 初始标记(STW):只标记GC Roots的直接关联的对象,因为只有一层对象所以STW时间很短。
- 并发标记:从GC Roots对象直接关联的对象开始遍历整个对象图,与用户线程并发执行。
- 重新标记(STW):由于并发标记是与用户线程并发执行的,那么要修正并发标记期间的已被标记但是又被用户修改的这部分对象。相比初始标记阶段,STW时间会相对长一些。
- 执行回收(STW):涉及对象的迁移,需要STW。在这个阶段,为了减少停顿时间可以将一次回收过程分为多次执。若在回收过程中对象分配过快,导致老年代内存填满,会退化为Seril Old单线程回收, 这个过程的时间与内存大小成正比的。期间会STW。
优点:
- GC周期短,低停顿:G1的思想就是持续的部分回收,每次都是清理垃圾集中的Region,不会全量回收,那么就不会像Serial Old 或 Parallel Old一样,在堆内存过大时,长时间STW阻塞。每次STW默认最多200ms。也是Mixed GC回收部分老年代Region的原因。
- 停顿时间可预测,我们可以通过参数来设置一次垃圾回收的时长。
- 更好的支持大内存。
三色标记算法
GC线程在打标B时,用户线程执行了对象属性的写操作,结果为上图的右部分。由于A对象为黑色,不会再被GC线程处理,又被改为不被B引用,那么C对象会被认为垃圾对象。但实际上却被A对象实际引用了,不应该被回收。对于这种漏标的情况,不同的垃圾回收器有不同的解决方式。基本都是基于读写屏障的方式。
读写屏障
var cField = B.c // 读
B.c = null // 写
A.c = C // 写
以写屏障为例,底层大概是这样的(类似AOP)
void field_store(Object* obj, Object new_fieldValue) {
pre_field_store(new_fieldValue); // 写前操作
*obj = new_fieldValue;
post_field_store(new_fieldValue); // 写后操作
}
CMS的处理方式
写屏障 + Incremental Update(增量更新)
若执行A.c = C,即对象的成员属性被改变,由于A是黑色,将C作为一个灰色对象放入remark_set中,在remakr阶段处理该集合中的对象即可。底层大概是这样的:
void post_field_store(Object new_fieldValue) {
if (!mark(new_fieldValue)) {
remakr_set.add(new_fieldValue); // 新值记录到remakr_set中
}
}
G1的处理方式
写屏障 + Snapshot At The Beginning(SATB快照)
若执行B.c = null,即对象的成员属性被改变,将修改前的对象C作为一个灰色对象放入remark_set中,在remakr阶段处理该集合中的对象即可。底层大概是这样的:
void pre_field_store(Object* field) {
remakr_set.add(* field); // 旧值记录到remakr_set中
}
(持续补充)