什么是三色标记算法

为什么会出现三色标记算法?

对于绝大部分垃圾收集器都是基于可达性分析算法来判断对象的存活状态;然后可达性分析算法理论上是一种基于一致性的快照中才能够进行分析,这就意味着需要进行STW(停顿用户线程进行垃圾收集标记)。

若堆中存储的对象很多,那么对于GC roots图结构越复杂,要标记更多的节点需要停顿更长时间,对于用户来说肯定是不友好,那么可通过削弱STW消耗的时间的话,那么收益也是系统级别的。三色标记算法是一种并发的可达性分析算法,可以削弱STW所耗费的时间。

什么是三色标记算法?

三色标记算法可以说是标记清除算法的一种升级版本,JVM在三色标记法中将所有对象(节点)划分为三类,分别用我们日常中的 三种颜色来表示每一类型的节点集合。其中三种颜色标记具体含义是什么呢?

1、节点被标记成白色

表示可达性分析初始阶段所有新创建的对象(节点)默认标记成白色状态,表示还没有被GC扫描过;如果可达性分析结束之后任是白色节点,则代表不可达,正常情况要被回收,异常情况看文章后面。

2、节点被标记成灰色

表示该节点至少还存在一个引用没有被扫描过,或者说是正在进行标记的节点,会被标记成灰色节点,也是一种中间状态最终会被标记成黑色或者停留在白色状态。

3、节点被标记成黑色

表示已经被GC扫描过的节点标记成黑色节点,此时黑色节点就是存活对象,不能被GC回收,黑色节点此时是安全的!

如下图所示:可达性分析初始阶段,程序开始扫描一次Root Set,初始阶段被标记成白色节点,放入白色节点集合中。如下所示,有7个Node节点。
在这里插入图片描述

三色标记算法的流程

1、初始阶段状态,都是白色节点状态,全部放入到白色节点集合中
在这里插入图片描述
2、然后从Root Set开始进行扫描,首先Node1Node5节点被扫描到,Node1Node5被标记成灰色节点如下图所示,从白色节点集合中挪到灰色节点集合中,对比第一步图示。

注意:这里只遍历一次Root Set集合,不是递归遍历

在这里插入图片描述

3、然后GC会从灰色节点开始继续向下扫描,也就是从Node1Node5(被标记成灰色的节点)开始,然后扫描到Node2Node6会被标记成灰色,然后Node2Node6从白色节点集合挪到灰色节点,然后Node1Node5因为已经被扫描过了会被标记成黑色,然后从灰色节点集合挪到黑色节点集合中,如下图所示:对比第2步图示
在这里插入图片描述
4、继续从灰色节点Node2Node6开始往下遍历,因为Node2Node6已经被扫描过了,所以会被标记成黑色节点,从灰色节点挪到黑色节点集合中,Node3节点被标记成灰色节点,从白色节点集合中挪到灰色节点集合中,其实就是重复上述第2步骤。
在这里插入图片描述
4、继续从灰色节点Node3开始遍历,知道最终没有了灰色节点就不会再往下遍历了,因为Node3被遍历过了,所以会被标记成黑色节点,从灰色节点集合中挪到黑色节点集合中,最终只会剩下两种状态的节点(只有黑和白两种状态节点)

黑色节点状态: 表示通过可达性分析算法最终存活下来的对象,该对象此时是安全的,不会被销毁

白色节点状态: 表示通过即将被回收的垃圾对象
在这里插入图片描述
6、最终回收玩垃圾节点就会只剩下被标记成黑色状态的节点,表示存在对象引用!这就是整个三色标记算法标记过程。
在这里插入图片描述

三色标记法无STW(削弱STW、并发执行)带来的问题

因为用户程序和标记是并发执行的,存在当扫描正在进行标记的时候,突然把新建了一个引用,引用了还没扫描过的白色节点,将还没来得及标记的对象赋值给了已经标记成黑色对象的节点了,此时存在两种情况:

第一种:不删除灰色节点对白色节点的引用(p指针)

被标记成黑色的节点突然新增一个对白色节点的引用,白色节点还未进行标记过,因为不删除灰色节点对白色节点的引用,所以这种情况不会出现什么问题,因为灰色节点还会往下继续查询标记白色节点。如下图所示:

从灰色节点Node2开始继续向下正准备要对Node3进行标记时,突然由于并发原因,黑色节点Node5新增了一个对白色节点Node3的引用,q指针指向了Node3,此时p指针还有指向Node3的引用,所以Node3节点是会被扫描到并被标记成灰色,最终标记成黑色,不会被当成垃圾,这种情况可以不用考虑。
在这里插入图片描述

第二种:删除灰色节点对白色节点的引用(p指针)

我们删除Node2Node3的引用(删除p指针),这样Node3节点就和Node2节点没有引用关系了,那么在可达性分析时,就标记不到Node3节点,默认还是白色状态的节点,那么就会被当成垃圾回收掉,但是此时Node3正在被黑色节点Node5引用着,如果Node3被当成垃圾回收掉了,问题就大了,Node3对象丢失了
在这里插入图片描述

总结:

综合上述两种情况,三色标记法并发执行虽然可以削弱STW(可视为无STW,就是并发操作),但是由于并发操作的原因也随之而来产生了问题——对象丢失,其实就是满足了两个条件导致的对象丢失造,归结为两个条件:

条件1: 白色节点被黑色节点引用(白色节点被挂在了黑色节点下,须知黑色节点是不会重新扫描的)

条件2: 灰色节点和可达关系的白色对象之间的应用遭到了破坏(删除灰白引用)

这两个条件其实也是三色标记算法种最不想要看到的局面,那么怎么避免解决这个问题呢?就得从这两个条件开始入手。

三色标记算法弱STW问题解决方案

强三色不变式(解决条件1)

必须按照强制性要求三色标记,黑色节点不能引用白色状态的节点,黑色只能引用灰色节点。

如图所示:Node1节点存在灰色节点的引用是允许的,但是存在白色节点的引用是不可以的,这是强三色硬性要求。
在这里插入图片描述

弱三色不变式(解决条件2)

黑色节点可以引用白色节点,但是白色节点必须存在其他的灰色节点对他的引用(不能删除灰色节点对白色节点的引用),或者可达它的链路上有存在灰色节点也是可以的。如下图所示:

第一种是不允许的,第二、三种是允许的, 因为白色节点Node2被灰色节点引用了,因为所有的灰色节点会遍历它下面所有的引用节点,所以Node2也会被标记成灰色,然后最终标记成黑色,存活下来,不会被当成垃圾回收掉。
在这里插入图片描述

强弱三色底层实现——写屏障

在这里插入图片描述

什么是写屏障?

早在HotSpot虚拟机中就出现过通过写屏障技术维护卡表(保存跨代引用的指针)状态。写屏障广泛用于在低延迟垃圾收集器中(CMS等),可以看作是虚拟机层面对引用类型字段赋值这个动作的一个AOP切面。此屏障和我们的JUC中的内存屏障是有区分的。既然是AOP切面,那么必然是和我们Spring提供的AOP类似,也有写前屏障,写后屏障(类比于环绕通知)。这里展示更新卡表的写后屏障(后置通知)代码如下:

void opp_field_store(oop* field, oop new_value){
	// 引用字段赋值
	*field = new_value;
	// 写后屏障,在这里完成卡表更新操作
	post_write_barrier(field, new_value);
}

 

屏障触发的时机

在这里插入图片描述
▶注意:栈是不会启用屏障保护机制,堆上面才有屏障机制

插入屏障(增量更新)

1、所谓插入屏障就是在A节点新建引用B节点的时候,B节点强制被标记成灰色状态(B节点挂在了A节点的下游,B节点标记成灰色状态)

2、插入屏障解决满足了强三色不变式,也就是新建的引用,不会再有黑色节点引用白色节点的情况发生了,因为插入屏障会强制让引用的对象标记成灰色状态,这样就严格遵守了三色标记法则

演示插入屏障

1、我们先截取某个状态下的扫描状态如图所示:然后以这个状态开始进行分析和理解什么是插入屏障;这里需要注意栈上没有屏障机制,堆中才会启用屏障机制。
在这里插入图片描述

2、在上述图扫描状态中,现在突然由于并发操作,外界想向Node1节点添加一个新的节点new_N1(栈),Node5节点添加一个新的节点new_N2(堆),如图所示:

注意:我们分析都是以黑色节点引用白色节点为例子进行分析
在这里插入图片描述

2.1、因为Node5是在堆中,会触发开启屏障机制(强制把引用的节点标记成灰色状态),然后新建立的引用的节点new_N2会被标记成灰色节点,如图所示:对比上述图示
在这里插入图片描述
2.2、然后Node1节点新建的节点new_N1实在栈中的,所以不会触发屏障机制,需要等待在垃圾回收前重新扫描一遍栈中所有的白色节点,注意这里会先把所有栈中的节点重置成白色节点,然后重新扫描,而且还必须STW,然后有引用的白色节点自然而然会被标记成黑色节点,所以new_N1最终也会被标记成黑色节点

注意扫描栈的标记是需要STW来保障数据的正确性,最终得到新建的两个节点都是存活节点,没有节点误判!
在这里插入图片描述

缺点:

结束还需要重置所有栈中黑色节点成白色节点,然后STW(保证没有新进入的白色节点或者黑色节点的引用)来重新扫描,大约需要耗时10~100ms
 

删除屏障(快照标记)

就是把需要被删除的节点强制标记成灰色节点,因为灰色节点最终是可以继续遍历的。删除屏障其实是满足了弱三色不变式(保护了灰色到白色节点绝不会断开关系,怎么样都会有一个灰色节点存在)

注意删除屏障栈,堆中都会启用!主要是借助STW来保存快照进行标记的。

演示删除屏障

1、我们也是通扫描的某个状态开始来演示分析什么是删除屏障,首先我们经过议论的扫描,Node1Node5标记成灰色状态,然后由于并发操作,删除了灰色节点Node1对白色节点Node2的引用,此时会触发删除屏障,强制要求Node2节点标记成灰色状态
在这里插入图片描述
2、如下图示:Node2被标记成了灰色状态的节点
在这里插入图片描述然后最终标记成黑色节点,如果被标记成黑色的Node2节点没有被别人引用的话,其实就成为了浮动垃圾,这次可以逃过垃圾收集,第二次才会被收集!但是如果有引用的话,那么就不会被当成浮动垃圾进行回收(就比如Node5去引用Node2节点)!
在这里插入图片描述

缺点:

回收率低,导致浮动垃圾的产生,只有等第二次垃圾回收才能被回收!

对比确定如下图所示:
在这里插入图片描述

混合式写屏障(上述两种结合体)

  • GC开始会将栈上的节点全部标记为黑色(不在进行第二次重复扫描,无需STW)
  • GC期间,任何在栈上创建的新对象,均为黑色
  • 被删除的对象标记成灰色
  • 新添加的对象标记成灰色

演示混合式屏障

开始栈中的节点都会被标记成白色状态节点,如下图所示:有限扫描全部栈中的节点,将可达节点标记成黑色(Node4Node5非可达),这也是最原始的混合式屏障机制。
在这里插入图片描述

场景一:节点被一个堆节点删除引用,成为栈节点的下游

在混合写屏障中,栈不开启屏障,因为全部被标记成黑色节点了,栈中黑色节点Node1,引用了堆中的白色节点Node7,然后Node6节点删除对应Node7的引用,这时会触发删除写屏障,强制将Node7标记成灰色的,然后加入到灰色标记集合中,这样栈中的黑色节点Node1就不会丢失对象Node7了,如下截图所示:
在这里插入图片描述
在这里插入图片描述

场景二:节点被一个栈节点删除引用,成为另一个栈节点的下游

在栈中新建一个节点Node8,因为是在栈上新生成的节点,在混合写屏障机制中,栈中的所有可达的节点被标记成黑色,所以Node8为黑色

然后黑色节点Node8引用到了Node3节点,不会触发任何屏障机制,直接引用即可,而且Node2删除引用也没关系,Node3还是安全的,如下图所示:
在这里插入图片描述

场景三:节点被堆节点引用,成为另一个堆节点的下游

在堆中新加入黑色节点Node8(以黑色节点考虑),然后引用堆的白色节点Node7,因为我么新加入引用,所以Node7节点标记成灰色,然后断开Node6的引用
在这里插入图片描述
在这里插入图片描述

场景四:节点从栈中删除引用,然后成为堆节点的下游

堆中节点Node6引用栈中的节点Node2,然后栈Node1删除对Node2引用,可以直接删除不用管,因为都是被标记成黑色的,然后堆中节点Node6删除对Node7的引用,会触发删除写屏障,从而Node7会强制标记成灰色。
在这里插入图片描述在这里插入图片描述

总结

并发标记通过两个方法保证节点不丢失,增量更新(插入写屏障)原始快照(删除写屏障),实现这两种方法底层都是写屏障实现!其中CMS是基于增量更新来做并发标记的,G1Shenandoah 则是使用原始快照来实现标记!

 

  • 9
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

魔道不误砍柴功

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值