为什么会出现三色标记算法?
对于绝大部分垃圾收集器都是基于可达性分析算法
来判断对象的存活状态;然后可达性分析算法理论上是一种基于一致性的快照中才能够进行分析,这就意味着需要进行STW
(停顿用户线程进行垃圾收集标记)。
若堆中存储的对象很多,那么对于GC roots
图结构越复杂,要标记更多的节点需要停顿更长时间,对于用户来说肯定是不友好,那么可通过削弱STW
消耗的时间的话,那么收益也是系统级别的。三色标记算法是一种并发的可达性分析算法,可以削弱STW
所耗费的时间。
什么是三色标记算法?
三色标记算法可以说是标记清除算法
的一种升级版本,JVM
在三色标记法中将所有对象(节点)划分为三类,分别用我们日常中的黑
白
灰
三种颜色来表示每一类型的节点集合。其中三种颜色标记具体含义是什么呢?
1、节点被标记成白色
表示可达性分析初始阶段所有新创建的对象(节点)默认标记成白色状态,表示还没有被GC
扫描过;如果可达性分析结束之后任是白色节点,则代表不可达,正常情况要被回收,异常情况看文章后面。
2、节点被标记成灰色
表示该节点至少还存在一个引用没有被扫描过,或者说是正在进行标记的节点,会被标记成灰色节点,也是一种中间状态最终会被标记成黑色或者停留在白色状态。
3、节点被标记成黑色
表示已经被GC
扫描过的节点标记成黑色节点,此时黑色节点就是存活对象,不能被GC
回收,黑色节点此时是安全的!
如下图所示:可达性分析初始阶段,程序开始扫描一次Root Set
,初始阶段被标记成白色节点,放入白色节点集合中。如下所示,有7个Node
节点。
三色标记算法的流程
1、初始阶段状态,都是白色节点状态,全部放入到白色节点集合中
2、然后从Root Set
开始进行扫描,首先Node1
、Node5
节点被扫描到,Node1
、Node5
被标记成灰色节点如下图所示,从白色节点集合中挪到灰色节点集合中,对比第一步图示。
注意:这里只遍历一次
Root Set
集合,不是递归遍历
3、然后GC
会从灰色节点开始继续向下扫描,也就是从Node1
、Node5
(被标记成灰色的节点)开始,然后扫描到Node2
、Node6会
被标记成灰色,然后Node2
、Node6
从白色节点集合挪到灰色节点,然后Node1
、Node5
因为已经被扫描过了会被标记成黑色,然后从灰色节点集合挪到黑色节点集合中,如下图所示:对比第2步图示
4、继续从灰色节点Node2
、Node6
开始往下遍历,因为Node2
、Node6
已经被扫描过了,所以会被标记成黑色节点,从灰色节点挪到黑色节点集合中,Node3
节点被标记成灰色节点,从白色节点集合中挪到灰色节点集合中,其实就是重复上述第2步骤。
4、继续从灰色节点Node3
开始遍历,知道最终没有了灰色节点就不会再往下遍历了,因为Node3
被遍历过了,所以会被标记成黑色节点,从灰色节点集合中挪到黑色节点集合中,最终只会剩下两种状态的节点(只有黑和白两种状态节点)
黑色节点状态: 表示通过可达性分析算法最终存活下来的对象,该对象此时是安全的,不会被销毁
白色节点状态: 表示通过即将被回收的垃圾对象
6、最终回收玩垃圾节点就会只剩下被标记成黑色状态的节点,表示存在对象引用!这就是整个三色标记算法标记过程。
三色标记法无STW(削弱STW、并发执行)带来的问题
因为用户程序和标记是并发执行的,存在当扫描正在进行标记的时候,突然把新建了一个引用,引用了还没扫描过的白色节点,将还没来得及标记的对象赋值给了已经标记成黑色对象的节点了,此时存在两种情况:
第一种:不删除灰色节点对白色节点的引用(p指针)
被标记成黑色的节点突然新增一个对白色节点的引用,白色节点还未进行标记过,因为不删除灰色节点对白色节点的引用,所以这种情况不会出现什么问题,因为灰色节点还会往下继续查询标记白色节点。如下图所示:
从灰色节点Node2
开始继续向下正准备要对Node3
进行标记时,突然由于并发原因,黑色节点Node5
新增了一个对白色节点Node3
的引用,q指针
指向了Node3
,此时p指针
还有指向Node3
的引用,所以Node3
节点是会被扫描到并被标记成灰色,最终标记成黑色,不会被当成垃圾,这种情况可以不用考虑。
第二种:删除灰色节点对白色节点的引用(p指针)
我们删除Node2
对Node3
的引用(删除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、我们也是通扫描的某个状态开始来演示分析什么是删除屏障,首先我们经过议论的扫描,Node1
、Node5
标记成灰色状态,然后由于并发操作,删除了灰色节点Node1
对白色节点Node2
的引用,此时会触发删除屏障,强制要求Node2
节点标记成灰色状态
2、如下图示:Node2
被标记成了灰色状态的节点
然后最终标记成黑色节点,如果被标记成黑色的
Node2
节点没有被别人引用的话,其实就成为了浮动垃圾,这次可以逃过垃圾收集,第二次才会被收集!但是如果有引用的话,那么就不会被当成浮动垃圾进行回收(就比如Node5
去引用Node2
节点)!
缺点:
回收率低,导致浮动垃圾的产生,只有等第二次垃圾回收才能被回收!
对比确定如下图所示:
混合式写屏障(上述两种结合体)
GC
开始会将栈上的节点全部标记为黑色(不在进行第二次重复扫描,无需STW
)GC
期间,任何在栈上创建的新对象,均为黑色- 被删除的对象标记成灰色
- 新添加的对象标记成灰色
演示混合式屏障
开始栈中的节点都会被标记成白色状态节点,如下图所示:有限扫描全部栈中的节点,将可达节点标记成黑色(Node4
、Node5
非可达),这也是最原始的混合式屏障机制。
场景一:节点被一个堆节点删除引用,成为栈节点的下游
在混合写屏障中,栈不开启屏障,因为全部被标记成黑色节点了,栈中黑色节点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
是基于增量更新来做并发标记的,G1
、Shenandoah
则是使用原始快照来实现标记!
推荐阅读文章
- 使用 Spring 框架构建 MVC 应用程序:初学者教程
- 有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
- 如何理解应用 Java 多线程与并发编程?
- Java Spring 中常用的 @PostConstruct 注解使用总结
- 线程 vs 虚拟线程:深入理解及区别
- 深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
- 10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
- “打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”
- Java 中消除 If-else 技巧总结
- 线程池的核心参数配置(仅供参考)
- 【人工智能】聊聊Transformer,深度学习的一股清流(13)
- Java 枚举的几个常用技巧,你可以试着用用
- 如何理解线程安全这个概念?
- 理解 Java 桥接方法
- Spring 整合嵌入式 Tomcat 容器
- Tomcat 如何加载 SpringMVC 组件