JVM三色标记法

Posted 嘿嘿嘿1212

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM三色标记法相关的知识,希望对你有一定的参考价值。

原因

在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 再深度扫描。

太上老君的“三色标记法”

​ 垃圾回收首先要知道到底哪些对象是已经死亡、可以被回收,当前主流的编程语言的垃圾收集器基本都基于可达性分析算法来判断一个对象是否可以被GC。可达性分析算法要求全过程都基于一个能保障一致性的快照中进行,也就是必须冻结正在运行的用户线程

​ 那么问题来了,为什么可达性分析算法在运行时,需要一个能保障一致性的快照?如果可达性分析算法运行的线程和用户的线程并发执行,会出现什么问题?这个停顿的时间能不能减少

​ 想要解决这些疑问,不得不深入了解下,可达性分析算法底层到底如何运作的,它是如何标记出哪些对象是存活的。洪爵在上篇文章《面试官:如何判定一个对象是否存活?》粗略的讲到可达性分析算法大概含义,本章节来深挖它底层逻辑,当面试官一脸自信的问到这个问题,认为你答不出来的时候,请狠狠的扇他一巴掌

​ 首先回顾下,可达性分析算法由一系列被称之为”GC Roots“的根对象作为起始节点集,从这些根节点开始,根据引用关系向下搜索,搜索过程所走过的路径称之为”引用链“,如果一个对象到RC Roots没有任何引用链相连,则表明这个对象不可达

​ 这还是比较抽象的概念,具体是如何运作我们引用三色标记法来说明:

  • 白色:尚未标记过
  • 灰色:已经访问过,但是还没有访问该对象的属性中直接引用的对象
  • 黑色:已经访问过了,该对象的属性直接引用对象也已经访问了

初始状态,即还没有开始可达性分析算法,所有的对象还处于白色标记中。

开始可达性分析算法后,把GC Roots直接关联到的对象加入到【灰色标记队列】中。

遍历对象A、C、K的属性中引用的对象,加进【灰色标记队列中】。

这个时候A、C、K对象的属性中直接引用的对象都访问完了,A、C、K三个对象从【灰色标记队列】中移除,加入【黑色标记队列】中,表示该对象是可达,即存活状态。

按照同样的动作,访问B、D属性中直接引用的对象,加入到【灰色标记队列】中。

标记B、D对象为黑色,加入到【黑色标记队列】。

以此类推,接下来分别是:

​ 最后A~I对象都被标记为黑色,而L、M、N三个对象被标记为白色没有标记白色这个动作,只是为了方便理解),代表没有访问过,可以被回收

​ 有的同学就会问了,**如果L、M、N对象中的任何一个后面又被GC Roots直接或者间接关联了呢?被回收岂不是不合理?**好的,问题非常好,这个是不会出现的情况,因为已经这三个对象已经丢失了,不知道它们的内存地址的位置。

​ 我们已经了解了可达性分析算法的具体运作过程,那么我们来分析一下,**为什么可达性分析算法在运行时,需要一个能保障一致性的快照?**举一个可达性分析算法和用户线程并发运行的例子,同样也继续使用三色标记法来为大家说明:

​ 假设可达性分析算法已经跑到如下图所示:

A、C、K三个对象已经被标记为黑色,即已经访问过,并且属性中引用的对象也访问了,即B和D对象,已经被标记为灰色,表示已经访问但B和D对象中属性引用的对象还有没有访问。这个时候用户并发线程做了一个操作,K对象中某个属性指向了对象G,然后断开了对象D引用对象G的关系

​ 而对象K已经被标记为黑色,代表该对象被访问过,并且属性中的引用对象也被访问过。所以对象G直到最后可达性分析算法结束,都不会被标记为灰色或者黑色。这就会出现回收了存活对象的问题,直接影响了程序的正确性

​ 可达性分析需要一个能保障一致性快照的场景下运行的原因已经找到了,那么我们有没有什么可以优化的点,让STW的时间缩短或者让可达性分析线程可以和用户线程并发运行呢

​ Wilson在1994年理论上证明,当且仅当同时满足以下两个条件时,才会产生“对象”消失的问题:

  • 一个或者多个黑色对象有了对白色对象的新引用
  • 删除了全部灰色对象到该白色对象的直接或者间接引用

​ 那要解决并发过程中因为引用关系导致的存活对象被回收问题,我们可以打破其中任何一个条件即可,打破第一条规则我们称之为增量更新、打破第二条规则我们称之为原始快照

​ 增量更新是啥意思呢,当黑色对象新增一个对白色对象的引用关系时,我们把这个引用关系给记录下来等并发标记完对象后,再将记录过的新的引用关系中黑色对象重新走一遍标记流程,可以简单的理解该黑色对象重新被标记为了灰色对象

​ 原始快照当灰色对象要删除对白色对象的引用,那么就将要删除的引用记录下来,在并发扫描结束后,会按照刚开始扫描那一刻的对象图来进行标记

​ 很多脑洞大开的童鞋就会想到,如果在增量更新后,又出现黑色对象对白色对象的新引用,那岂不是俄罗斯套娃,永无止境?其实有很多办法可以解决,比如说对重新标记的场景进行STW等等。

​ 到了文章末尾,洪爵又有了新的问题提出给大家**,增量更新和原始快照是如何实现的,如何在并发的情况下进行记录?**别着急,洪爵下一篇文章告诉你(禁止套娃)。


愿每个人都能带着怀疑的态度去阅读文章并探究其中原理。

道阻且长,往事作序,来日为章。

期待我们下一次相遇!

以上是关于JVM三色标记法的主要内容,如果未能解决你的问题,请参考以下文章

带颜色的JVM:三色标记详解

JVM垃圾回收的“三色标记算法”实现,内容太干

浅谈JVM GC三色标记算法

你对JVM三色标记的理解嘛?

太上老君的“三色标记法”

七天入门Go语言 GC垃圾回收三色标记 | 第七天