漏标和多标
对于错标其实细分出来会有两种情况,分别是:漏标和多标
多标-浮动垃圾
如果标记执行到 E 此刻执行了 object.E = null
在这个时候, E/F/G 理论上是可以被回收的。但是由于 E 已经变为了灰色了,那么它就会继续执行下去。最终的结果就是不会将他们标记为垃圾对象,在本轮标记中存活。
在本轮应该被回收的垃圾没有被回收,这部分被称为“浮动垃圾”。浮动垃圾并不会影响程序的正确性,这些“垃圾”只有在下次垃圾回收触发的时候被清理。
还有在,标记过程中产生的新对象,默认被标记为黑色,但是可能在标记过程中变为“垃圾”。这也算是浮动垃圾的一部分。
漏标-读写屏障
写屏障(Store Barrier)
给某个对象的成员变量赋值时,其底层代码大概长这样:
/** * @param field 某个对象的成员属性 * @param new_value 新值,如:null */ void oop_field_store(oop* field, oop new_value) { *fieild = new_value // 赋值操作 }
所谓写屏障,其实就是在赋值操作前后,加入一些处理的逻辑(类似 AOP 的方式)
void oop_field_store(oop* field, oop new_value) { pre_write_barrier(field); // 写屏障-写前屏障 *fieild = new_value // 赋值操作 pre_write_barrier(field); // 写屏障-写后屏障 }
写屏障 + SATB
当对象E的成员变量的引用发生变化时(objE.fieldG = null;),我们可以利用写屏障,将E原来成员变量的引用对象G记录下来:
void pre_write_barrier(oop* field) { oop old_value = *field; // 获取旧值 remark_set.add(old_value); // 记录 原来的引用对象 }
【当原来成员变量的引用发生变化之前,记录下原来的引用对象】
这种做法的思路是:尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB) ,当某个时刻 的GC Roots确定后,当时的对象图就已经确定了。
比如 当时 D是引用着G的,那后续的标记也应该是按照这个时刻的对象图走(D引用着G)。如果期间发生变化,则可以记录起来,保证标记依然按照原本的视图来。
值得一提的是,扫描所有GC Roots 这个操作(即初始标记)通常是需要STW的,否则有可能永远都扫不完,因为并发期间可能增加新的GC Roots。
SATB破坏了条件一:【灰色对象 断开了 白色对象的引用】,从而保证了不会漏标。
一点小优化:如果不是处于垃圾回收的并发标记阶段,或者已经被标记过了,其实是没必要再记录了,所以可以加个简单的判断:
void pre_write_barrier(oop* field) { // 处于GC并发标记阶段 且 该对象没有被标记(访问)过 if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) { oop old_value = *field; // 获取旧值 remark_set.add(old_value); // 记录 原来的引用对象 } }
写屏障 + 增量更新
当对象D的成员变量的引用发生变化时(objD.fieldG = G;),我们可以利用写屏障,将D新的成员变量引用对象G记录下来:
void post_write_barrier(oop* field, oop new_value) { if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) { remark_set.add(new_value); // 记录新引用的对象 } }
【当有新引用插入进来时,记录下新的引用对象】
这种做法的思路是:不要求保留原始快照,而是针对新增的引用,将其记录下来等待遍历,即增量更新(Incremental Update)。
增量更新破坏了条件二:【黑色对象 重新引用了 该白色对象】,从而保证了不会漏标。
读屏障(Load Barrier)
oop oop_field_load(oop* field) { pre_load_barrier(field); // 读屏障-读取前操作 return *field; }
读屏障直接针对第一步 var objF = object.fieldG;
,
void pre_load_barrier(oop* field, oop old_value) { if($gc_phase == GC_CONCURRENT_MARK && !isMarkd(field)) { oop old_value = *field; remark_set.add(old_value); // 记录读取到的对象 } }
这种做法是保守的,但也是安全的。因为条件二中【黑色对象 重新引用了 该白色对象】,重新引用的前提是:得获取到该白色对象,此时已经读屏障就发挥作用了。
三色标记法与垃圾回收器
增量更新: CMS原始快照(STAB): G1,Shenandoah
参考文档
- 《深入理解 JVM 虚拟机-第三版》周志明