原因

在CMS等并发收集器,并发标记的过程中需要对对象进行标记,用于区别对象。防止多标,漏标等情况。

三色标记

三色标记法就是指将GC roots可达性算法分析遍历对象过程中将各个对象,按照”是否访问过“标记成不同的三种颜色(可以理解为类似于成员变量)。

  • 黑色
    表示对象已经被垃圾收集器访问过(扫描过),并且这个对象的所有引用都已经被扫描过,它是安全存活的(不是垃圾对象),如果其他对象引用指向了黑色对象,是无需重新扫描的,黑色对象不可能直接(不经过灰色对象)指向白色对象
  • 灰色
    表示对象已经被垃圾收集器访问过,但是这个对象上至少存在一个引用还没有被扫描过。(表示正在扫描该对象的引用)
  • 白色
    表示对象尚未被垃圾收集器访问过,在可达性分析刚刚开始时的阶段,所有的对象都是白色的,如果在分析结束的阶段,还是白色的对象,表示不可达(被清理)。

存在问题与解决方法

多标(浮动垃圾)

浮动垃圾并不会影响垃圾回收的正确性,可以等到下一轮垃圾回收中再清除。

原因:

  • 并发标记过程中,由于用户线程运行导致部分局部变量(GC root)被销毁,导致该局部变量(GC root)所引用的对象(被扫描过,黑色)成为浮动垃圾。
  • 并发标记或并发清理,由于用户线程运行产生的新对象,通常被直接全部标记为黑色,而这些对象这此期间也会变成垃圾,算是浮动垃圾的一部分。

解决:

  • 本轮GC将不会回收这部分内存,浮动垃圾并不会影响垃圾回收的正确性,只需要等到下一轮GC才被清除

漏标(读写屏障)

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决.

漏标原因(二者必须同时存在时,才会漏标):

  • 当黑色对象被插入新的指向白色对象的引用关系时,此时将会产生不扫描新插入的白色对象。(使用白色对象
  • 当有灰色对象的白色对象的引用被删除时,无法感知白色是否被其他对象引用。(产生白色对象

注意:白色对象只会产生于灰色对象引用关系中,这就导致黑色对象关联的白色对象一定来自灰色对象,而新new出来的对象会被直接标记为黑色

解决方法:

所以漏标可以在产生白色对象的地方制止,也可以在使用白对象的地方制止。由此产生了两种方法。

  • 增量更新(Incremental Update)
  • 原始快照(Snapshot At The Beginning或SATB)写屏障
增量更新

针对白色对象的使用方

当黑色对象插入新的指向白色对象的引用关系时,将新插入的引用关系记录下来,等并发扫描结束之后,再将记录过的黑色对象为根,重新扫描一次。

保证在扫描时线程正在扫描灰色对象引用的对象时,黑色对象引用了白色对象导致漏标问题

简单理解:黑色对象被新插入了指向白色对象的引用后,就变成灰色对象

原始快照(SATB)

针对白色对象的产生方

当灰色对象删除指向白色对象的引用关系时,将要删除的引用关系记录下来(记录灰色对象指向白色对象的关系),在并发扫描结束后。再将记录过的引用关系的灰色对象为根,重新扫描一次(此时是使用之前保存的关系进行扫描的,即还原到未删除时候的快照,扫描的是各种被删除的对象),扫描到白色对象再将白色对象直接标记为黑色。通常使用写屏障实现的。

保证在扫描时线程正在扫描灰色对象引用的对象时,灰色对象的引用改变导致某些白色对象不可达(用户线程正在运行)

注意: 标记为黑色,该对象就会在本轮gc中存活下来,下一轮gc重新扫描,这个对象就有可能是浮动垃圾。

写屏障

在底层代码中进行类似AOP切面的代码,即在赋值前后进行操作。

写屏障实现SATB

当对象的成员变量的引用发生变化时,例如删除引用(赋值为null),我们可以利用写屏障,将原有成员变量的引用记录下来

 void pre_write_barrier(oop* field)  {   oop old_value = *field; // 获取旧值   remark_set.add(old_value); // 记录原来的引用对象  }
写屏障实现增量更新

当对象的成员变量的引用变化,例如新增引用(将之前为null值赋值),就可以利用写屏障,将新的成员变量引用对象记录下来。

读屏障

与写屏障类似,即在读值前后进行操作。

读屏障更加直接,当读取成员变量时,就一律记录下来。

void pre_load_barrier(oop* field) {oop old_value = *field;remark_set.add(old_value); // 记录读取到的对象}

结语

现代追踪式(可达性分析)的垃圾回收器,几乎都借鉴了三色标记法的算法思想,实现的方式也都差不多。比如白色/黑色 集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可 以是广度/深度遍历等等。

读写屏障在Java Hotsot VM中,并发标记时对漏标处理方案:

  • CMS:写屏障+增量更新
  • G1,Shenandoah:写屏障+SATB
  • ZGC:读屏障
    工程实现中,读写屏障还有其他功能,比如写屏障可以用于记录跨代/区引用的变化,读屏障可以用于支持移动对象的并 发执行等。功能之外,还有性能的考虑,所以对于选择哪种,每款垃圾回收器都有自己的想法。

为什么G1用SATB?CMS用增量更新?

我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描 被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代 区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC 再深度扫描。