三色标记法、写屏障、增量更新和原始快照(CMS和G1)
漏标和错标
错标:原本不是垃圾,但是GC的过程中,用户线程将其引用关系修改,导致GC Roots不可达,成为了垃圾。这种情况还好一点,无非就是产生了一些浮动垃圾,下次GC再清理就好了。
漏标:原本是垃圾,但是GC的过程中,用户线程将引用重新指向了它,这时如果GC一旦将其回收,将会导致程序运行错误。
因此,在cms重新标记阶段和G1的最终标记阶段就是解决漏标问题,而错标问题,就等待下次GC再清理。
错标:cms采用写屏障+增量更新解决漏标,G1采用写屏障+原始快照解决漏标。
三色标记法
• 白色(White):初始状态下,所有对象都是白色的。白色的对象表示尚未被访问或标记。如果在标记阶段结束后,某个对象依然是白色,则认为它是不可达的,可以被回收。
• 灰色(Gray):灰色的对象表示它已经被访问并且被标记为存活,但其所有的引用对象还没有被递归标记。也就是说,垃圾收集器已经知道这个对象是存活的,但尚未检查其引用的其他对象。
• 黑色(Black):黑色的对象表示它已经被访问且其所有引用的对象都已经被递归标记为存活。黑色的对象不会被重新扫描,它们已经完成了标记过程。
写屏障
这个写屏障指的并不是并发编程里的写屏障。这里的写屏障指的是属性赋值的前后加入一些处理,类似于AOP。
通过写屏障来实现cms的增量更新和G1的原始快照。
给某个对象的成员变量赋值时,其底层代码大概长这样:
void oop_field_store(oop* field, oop new_value) {
*field = new_value; // 赋值操作
}
所谓写屏障就是在复制前后增加一些操作(类似以AOP概念)
void oop_field_store(oop* field, oop new_value) {
pre_write_barrier(field); // 写屏障-写前操作
*field = new_value;
post_write_barrier(field, value); // 写屏障-写后操作
}
写屏障实现SATB
当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用对象D记录下来:
void pre_write_barrier(oop* field) {
oop old_value = *field; // 获取旧值
remark_set.add(old_value); // 记录原来的引用对象 记录B-D 应用,当并发标记扫描结束,重新标记阶段我们可以从remark_set 取出B-D 应用直接将D对象标记为黑色 (G1垃圾收集器采用次算法)
}
写屏障实现增量更新
当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来:
void post_write_barrier(oop* field, oop new_value) {
remark_set.add(new_value); // 记录新引用的对象 记录A->D 的引用
}
增量更新和原始快照
为了更好的理解,首先我们来分析这张图,A是黑色,表示A和它的引用B都被访问到了,A不会再被重新扫描了。访问到了B,此时已经访问完C(包括其引用,因此是黑色),开始准备访问B的引用D,因此B是灰色,D是白色。此时B->D的引用被删除了,A->D的引用被建立。导致,A不会被再次扫描,D又无法通过B可达,因此在并发标记阶段,D被视为垃圾。因此就有了重新标记/最终标记阶段,增量更新和原始快照都是为其做铺垫的。
- 增量更新:是CMS垃圾收集器采用的标记策略。如果在并发标记期间,应用程序线程让某个存活对象新引用了一个未被标记的对象(A->D),增量更新策略依赖写屏障来跟踪对象引用的增加。每当应用程序创建一个新的引用,写屏障会将这些修改记录下来,并在重新标记阶段处理这些新引用,确保新引用的对象不会被漏标。
- 原始快照(SATB):是G1垃圾收集器采用的一个标记策略。如果在并发标记期间,应用程序线程删除了某个对象的引用(B->D),导致该对象在之后的扫描中变得不可达,SATB策略会通过写屏障机制记录删除前的引用关系。通过这种方式,在最终标记过程,它仍然会被视为存活对象并进行标记,避免漏标。即:当并发标记阶段开始时,垃圾收集器会将整个堆视为一个“静止的快照”,并基于这个快照来确定哪些对象是存活的。任何在标记阶段开始时从GC Roots可达的对象都会被标记为存活对象。
参考:
- ChatGPT-4o
- CMS与三色标记算法
- JVM 垃圾回收算法详解(CMS、三色标记)