g1采用satb(起始快照)方案,在引用被覆盖前通过写前屏障记录旧值,确保初始标记时存活的对象不被漏标;cms采用增量更新(iu)方案,在黑色对象新增指向白色对象的引用时将其重新标记为灰色。

三色标记不是画颜色,而是建状态图:白色是未入图节点,灰色是已入图但出边未遍历的中间节点,黑色是入图且所有出边已遍历完毕的终态节点。从图论角度看,并发标记本质是在一个动态演化的有向图上做增量可达性分析——用户线程持续修改边(引用),而 GC 线程按拓扑序推进标记。漏标与错标,就是这个过程中图结构扰动导致的状态不一致。
漏标:图中出现“不可达路径的伪断连”
漏标对应图论中一种特定失效模式:某白色节点 D 原本通过灰色节点 B → D 可达;并发中,B 删除该边(B→D 断开),同时黑色节点 A 新增边(A→D)。此时 D 在图中仍被 A 指向、逻辑可达,但标记机由于 A 已为黑色不再扫描其出边,D 永远无法被染灰,最终被误判为不可达。
这等价于:在一次 DFS 遍历中途,图中新增了一条从已访问完成子图(黑色集)指向未访问子图(白色集)的后向边,且原有一条从正在遍历子图(灰色集)指向该白色节点的前向边被删除。DFS 树无法回溯补全,造成覆盖缺失。
解决方案聚焦于“保连通”:
- 增量更新(CMS):检测到 A→D 新边时,将 A 从黑色“降级”为灰色,使其重新入队参与扫描——相当于对 DFS 树做局部回滚与重展开。
- 原始快照 SATB(G1):在标记开始前对整个引用图拍快照,把所有初始存在的边视为“必须覆盖”。并发中 B→D 断开时,记录旧边 B→D;最终标记阶段,以该边终点 D 为起点反向传播存活性——相当于用快照构建一个保守的可达闭包,宁可多标(浮动垃圾),不许漏标。
错标:图中存在“虚假存活路径”
错标即浮动垃圾,对应图中一条路径虽曾存在,但已失效。例如:灰色节点 C 被标记时由 A→C 达到;随后用户线程执行 A = null,A 不再指向 C;但 C 已被染灰并终将变黑。C 在本轮 GC 中不会被回收,尽管它实际已不可达。
这等价于:DFS 过程中某个节点被纳入遍历栈(灰色),但其入边在后续被全部删除,导致它在运行时图中成为孤点。标记系统因“已入栈”而保留其状态,违背了实时可达性。
错标无需修复,是并发标记可接受的代价:
- 它不破坏内存安全,只推迟回收(C 下轮 GC 必然白色且无入边,被正确回收);
- 修复它需在每次引用变更时同步验证整条路径有效性,开销远超收益;
- 所有主流收集器(CMS/G1/ZGC)均容忍错标,通过“下一轮清理”自然收敛。
写屏障:图结构变更的拦截与登记机制
写屏障不是并发编程里的内存屏障,而是图操作的“钩子函数”。它在每次引用赋值(即有向边增删)时触发,把变更事件登记进专门的数据结构,供重新标记阶段消费。
两类写屏障对应两种图维护策略:
-
写后屏障(post-write barrier):用于增量更新。当
a.field = d执行后触发,检查 a 是否为黑色、d 是否为白色;若是,则将 a 加入灰色队列——捕获“新边引入”事件。 -
写前屏障(pre-write barrier):用于 SATB。当
b.field = null执行前触发,读取原值 d,若 d 为白色,则将其加入 SATB 缓冲区——捕获“旧边消失”事件,保住被删边所指对象的存活证据。
二者都依赖 RSet(Remembered Set)或类似结构索引跨区域引用,确保登记高效,不遍历全堆。
为什么 G1 选 SATB 而非增量更新?
从图规模看:G1 分代+分区,跨区引用频繁。增量更新要求每次新边都触发黑色节点重扫描,可能引发大量重复遍历;SATB 则只登记被删边的终点,数量更少、更可控。
从收敛性看:SATB 的快照语义天然适配 G1 的“预测式并发收集”——它假设所有初始存活对象及其衍生引用都应被本轮保护,符合 G1 控制停顿时间的强目标。而增量更新的反复重扫会放大 STW 波动。
本质上,这是图维护策略的选择:SATB 是“基于快照的保守闭包”,增量更新是“基于变更的动态修正”。前者空间换确定性,后者时间换精度。











