
前言
大家好,这里是程序员阿亮,今天我在复习三色标记法的时候,发现之前学得不够深,今天来带大家深入了解三色标记法及其解决漏标的方法--增量更新、原始快照
在现代垃圾回收(GC)器的设计中,如何减少 "Stop The World" (STW) 的停顿时间是核心挑战。为了实现并发标记(Concurrent Marking) ------即让垃圾回收线程和用户线程同时运行,CMS、G1 以及 Go 的 GC 都引入了三色标记法(Tri-color Marking)。
一、三色标记法是什么?
传统的标记-清除算法在执行时需要暂停所有用户线程,这在大内存场景下是不可接受的。三色标记法通过将对象分为三种状态,逻辑上将"标记"过程变为一个可分步、可并发的过程。
1.1. 三种颜色的定义
-
⚪ 白色 (White):
-
表示对象尚未被垃圾回收器访问过。
-
在标记开始时,所有对象都是白色。
-
在标记结束时,如果对象仍然是白色,则代表不可达,将被回收。
-
-
⚫ 黑色 (Black):
-
表示对象已经被访问过,且该对象引用的所有其他对象也都已经被访问过。
-
黑色对象是安全存活的,且不会被重新扫描。
-
-
🔘 灰色 (Grey):
-
表示对象已经被访问过,但它引用的对象中至少有一个还没有被访问(即还在扫描其子节点)。
-
灰色是黑色和白色之间的"波前"或中间状态。
-
1.2.三色标记法流程

1.2.1 初始标记
在初始标记阶段会以GC ROOTS为起点,将GCROOTS的直接引用对象标记为灰色。这个阶段是需要STW(Stop The Word)的
1.2.2 并发标记
在这个阶段,会把灰色对象的所有引用对象标记为灰色,并且如果灰色对象的所有子对象都标记完了,那么就会把这个对象标记为黑色。
并且这个阶段是会和用户线程并发运行的,也就是说用户线程可能改变对象的引用状态,这个时候会通过写屏障去记录这些修改,通过增量更新或者原始快照等方案在重新标记阶段解决漏标问题。
为捕获引用变更,JVM 在对象引用赋值操作中插入写屏障(Write Barrier),其行为取决于采用的漏标防御方案:
| 方案 | 写屏障触发时机 | 捕获内容 | 并发标记阶段行为 |
|---|---|---|---|
| 增量更新 | 赋值后 | 检测"黑色→白色"新增引用 | 立即将黑色对象重新标灰,加入灰色队列 |
| 原始快照(SATB) | 赋值前 | 捕获被覆盖的旧引用值 | 将旧引用指向的白色对象加入待标记队列 |
1.2.3 重新标记
重新标记阶段会来标记并发标记被修改的对象的引用关系以及未被遍历到的对象(并发标记阶段程序一直在运行,标记不一定能全部标记到),通过增量更新或者原始快照来解决漏标问题。
二、三色标记法的漏标问题
2.1 漏标问题发生的原因
漏标问题发生必须同时满足俩个条件
在并发标记阶段,GC线程与用户线程同时运行,可能导致存活对象被错误回收 的漏标问题。漏标发生的必要条件(需同时满足):
- 黑色对象新增指向白色对象的引用(插入新引用)
- 灰色对象断开对同一白色对象的引用(删除旧引用)
此时白色对象既不会被灰色对象继续标记,也不会被已"完成扫描"的黑色对象重新访问,导致漏标
2.2 增量更新
它通过破坏第一个导致漏标的条件--黑色对象新增指向白色对象的引用(插入新引用)
当我们的黑色对象指向新的白色对象的时候,会由于黑色对象不再扫描导致一个漏标问题
那么这个时候我们可通过写屏障记录有指向新白色对象的黑色对象,在重新标记阶段进行增量扫描
我们的CMS就是使用增量更新策略。
2.3 原始快照
原始快照通过解决第二个条件--灰色对象断开对同一白色对象的引用(删除旧引用)来解决漏标问题。
在我们进行并发标记前会生成一个逻辑快照,去记录所有的对象的初始引用关系。
并承诺这些对象最终都会被标记为存活,绝不回收。
具体说就是,当我们在并发标记阶段,对象的引用关系发生了变化,断开了引用,那么我们会通过快照去查看这个对象的引用是否原本指向我们的白色对象,如果是指向白色对象,那就通过写屏障记录起来,并在重新标记阶段去标记,确保绝对不漏删,但是可能导致一定的浮动垃圾,不过可以在下次GC清理掉。
三、总结
三色标记法是并发垃圾回收的基石,而写屏障则是保证其正确性的卫士。
-
如果你追求低延迟且能容忍一定的浮动垃圾,G1 的 SATB 策略更优(配合 TLAB 分配,效率极高)。
-
如果你追求极致的内存回收率,早期的 CMS 增量更新 策略则更为精准。
