07-三色标记的问题
2026/9/27大约 4 分钟
一、先回顾:三色标记与漏标、浮动垃圾
| 概念 | 定义 | 后果 |
|---|---|---|
| 漏标 | 并发标记时,黑色对象(已扫描)新引用了白色对象(未扫描),且这个引用关系没有被记录下来。 | 致命错误:白色对象会被 GC 当作垃圾回收,但应用线程实际还在使用它,导致程序崩溃。 |
| 浮动垃圾 | 并发标记时,某个对象被标记为黑色(存活),但在标记完成后、实际清理前,应用线程断开了对该对象的引用。 | 可接受:这个对象本应被回收,但本次 GC 会放过它,留到下一次 GC 才清理,只是降低了回收效率。 |
二、CMS 的处理方式:增量更新(Incremental Update)
CMS 采用 增量更新(Incremental Update) 来解决漏标问题。
1. 核心机制:写屏障(Write Barrier)
CMS 在引用赋值时(如 a.field = b),如果满足以下条件,就会触发写屏障记录:
- 条件:黑色对象
a正在写入一个引用,指向白色对象b。 - 动作:将黑色对象
a重新标记为灰色,并放入一个队列(mod_union_table)。
2. 处理步骤
并发标记阶段:应用线程和标记线程并发执行。如果黑色对象新增了对白色对象的引用,CMS 会“回退”这个黑色对象为灰色,让它“重新扫描”一遍。
重新标记阶段(Stop The World):CMS 会 STW 暂停所有应用线程,然后重新扫描所有被“增量更新”记录下来的灰色对象,确保所有漏标的对象都被正确标记为存活。
3. 优缺点
- ✅ 优点:实现相对简单,能保证不遗漏任何存活对象(SATB 思想的一种变体)。
- ❌ 缺点:因为采用“重新扫描”方式,重新标记阶段需要 STW,且如果并发期间引用变化频繁,需要扫描的对象会增多,导致 STW 时间变长(这就是 CMS 的“Stop-The-World 时间不可控”的根源)。
三、G1 的处理方式:原始快照(SATB, Snapshot-At-The-Beginning)
G1 采用 原始快照(SATB) 来解决漏标问题。
1. 核心思想
SATB 的核心思想是:在并发标记开始时(即“快照”时刻),逻辑上冻结所有对象的引用关系图。整个并发标记过程基于这个“快照”来标记存活对象。
2. 核心机制:写屏障 + SATB 队列
G1 同样在引用赋值时(如 a.field = b)触发写屏障,但规则和 CMS 相反:
- 条件:当灰色或白色对象的引用被删除时(即
a.field = c,导致旧引用b断开),G1 会将被删除的旧引用b记录到SATB 队列中。 - 动作:把旧引用指向的对象
b记录并标记为存活(即使b实际上已经没用了)。如果c真的是存活对象,那么快照时刻必然有其他灰色对象引用它,它会通过正常的三色标记流程变黑;如果没有任何其他引用,那即使现在被a引用,也可能是临时引用,不值得为此打破 SATB 的规则。
3. 处理步骤
并发标记阶段:G1 不关心黑色对象新增了什么引用,它只关心“在快照开始时存活的对象,在标记结束前都视作存活”。
最终标记阶段(Stop The World):G1 会 STW 处理 SATB 队列中的记录,将所有在快照时刻有引用的对象都标记为存活。
4. 优缺点
- ✅ 优点:STW 时间非常短且可预测,因为 SATB 队列的扫描速度极快(几乎只扫描记录数量,不会像 CMS 那样重扫整个堆)。
- ❌ 缺点:产生更多浮动垃圾。因为 SATB 会保守地保留“快照时刻存活的所有对象”,即使这些对象在标记过程中变成了垃圾,也会被当作存活对象,留到下一次 GC 再清理。
四、关键对比表(CMS vs G1)
| 维度 | CMS | G1 |
|---|---|---|
| 屏障机制 | 增量更新(写屏障) | SATB(写屏障 + 快照队列) |
| 触发条件 | 黑色对象 新增 引用指向白色对象 | 灰色/白色对象的 旧引用被删除 |
| 处理方法 | 把黑色对象变灰,加入队列重新扫描 | 把被删除的旧引用对象加入 SATB 队列 |
| STW 阶段 | 重新标记阶段(扫描变灰的对象,耗时较长) | 最终标记阶段(处理 SATB 队列,耗时极短) |
| 浮动垃圾 | 较少 | 较多(因为保守保留快照存活对象) |
| 漏标风险 | 无(通过重扫解决) | 无(通过快照保守标记解决) |