三色标记算法

一、补全核心定义:三色状态的"动态性"与"不变式"

原文定义了三种颜色,这里补充两个支撑算法正确性的关键概念:

1.1 三色不变式(Tri-color Invariant)

为了保证标记结果的正确性,并发标记过程中必须始终维护以下两条不变式之一:

  • 强三色不变式黑色对象不能直接引用白色对象。如果存在黑色→白色的引用,白色对象就会被错误地遗漏。

  • 弱三色不变式所有被黑色对象引用的白色对象,必须能从某个灰色对象通过引用链到达。即白色对象要么不被黑色引用,要么被黑色引用时,一定有另一个灰色对象也在引用它(或能间接到达它)。

SATB和增量更新,本质都是通过不同的手段来维护这两条不变式。

1.2 "浮动垃圾"的本质

原文提到了浮动垃圾,这里明确它的来源:

  • 定义 :在并发标记开始时 是可达对象(被快照记录),但在并发标记期间被用户线程改为不可达的对象。

  • 为什么存在:SATB机制保证了快照中的所有对象都会被标记为存活,即使它们在标记过程中变成了垃圾。

  • 影响:这些浮动垃圾无法在本次GC中被回收,必须等到下一次GC。这是G1和CMS内存占用有时看起来"回收不彻底"的核心原因之一。


二、SATB vs 增量更新:从"拦截点"看本质差异

原文从特性上做了对比,这里从写屏障拦截的具体操作来拆解,这是理解两者对性能影响的关键。

2.1 SATB(Snapshot-At-The-Beginning)------关注"删除"
  • 拦截操作reference.field = new_value(引用赋值)

  • 屏障逻辑

    java

    复制代码
    // 伪代码:在赋值前执行
    if (old_value != null && old_value.isWhite()) {
        // 将即将被覆盖的旧引用对象(old_value)记录到SATB日志中
        satb_log.add(old_value); 
    }
    // 执行实际赋值
    reference.field = new_value;
  • 本质 :它关心的是**"引用被切断前,被切断的那个对象"**。即使这个对象后续再也没有被任何灰色/黑色对象引用,SATB日志也会保留它,使其在本轮GC中仍然存活。

  • 性能特点"拦前" 操作,只需记录旧值,逻辑简单,对赋值操作的开销相对固定。

2.2 增量更新(Incremental Update)------关注"新增"
  • 拦截操作reference.field = new_value(引用赋值)

  • 屏障逻辑

    java

    复制代码
    // 伪代码:在赋值后执行
    // 执行实际赋值
    reference.field = new_value;
    // 如果赋值方是黑色对象,且新值(new_value)是白色
    if (reference.isBlack() && new_value.isWhite()) {
        // 将黑色对象(reference)重新标记为灰色,加入标记栈
        mark_stack.push(reference); 
    }
  • 本质 :它关心的是**"谁引用了新对象"**。通过将黑色对象"降级"为灰色,强制GC重新扫描这个黑色对象的所有引用,从而确保新建立的引用链被正确标记。

  • 性能特点"拦后" 操作,且需要判断赋值方的颜色,如果赋值频繁且赋值方多为黑色对象,会导致大量对象被反复"降级"和重扫,增加GC负担。

2.3 为什么G1最终选择了SATB?

G1在早期版本曾尝试过增量更新,但最终在主流版本中稳定使用SATB,原因在于:

  • RSet的维护成本:G1的Region模型已经通过RSet维护跨代/跨区引用。SATB的"浮动垃圾"特性,与G1"分批回收、容忍部分垃圾"的设计哲学更匹配。

  • 避免"标记抖动":增量更新在频繁引用修改的场景下,可能导致同一个黑色对象被反复标记为灰色、扫描、再变黑,产生不必要的重复扫描。


三、深入写屏障:不只是"记录",还有"剪枝"

原文提到写屏障,这里补充一个容易被忽视的细节:写屏障不仅是"记录者",还是"优化者"

3.1 屏障的"过滤"机制

并不是所有引用修改都会被记录到SATB日志中。为了减少日志体积和后续处理开销,屏障会进行过滤:

  • 如果旧引用对象已经是灰色或黑色:说明它已经被标记队列处理过或正在处理,不再记录(避免重复)。

  • 如果新引用对象是白色,且赋值方是黑色:在SATB中不记录新增(因为SATB只关心被删除的引用),而是依赖已记录的旧引用链来保证可达性。这正是SATB"保守标记"的体现。

3.2 卡表(Card Table)与RS的配合

在G1中,写屏障除了处理SATB日志,还要维护RSet

  • 当发生引用赋值时,写屏障会判断这个赋值是否跨Region。

  • 如果是跨Region,需要将目标Region的RSet中记录下"源Region引用了你"。

  • 这个操作称为**"脏卡"**标记,后续GC线程通过扫描脏卡来更新RSet,避免在STW阶段全量扫描。

调优启示 :如果应用存在大量的跨Region引用修改(如频繁操作大型缓存Map),写屏障的开销会显著增加。此时可以通过-XX:G1ConcRefinementThreads调整RSet更新的并发线程数,或优化数据结构减少跨区引用。


四、三色标记在GC日志中的"蛛丝马迹"

理解三色标记后,再去看GC日志,你就能"看"到算法的执行痕迹。以G1为例:

4.1 并发标记阶段的日志

text

复制代码
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0012 secs]
[GC concurrent-mark-start]
[GC concurrent-mark-end, 0.0356 secs]
  • concurrent-mark阶段:就是三色标记中**"灰色→黑色遍历"**的核心过程。

  • 如果这个阶段耗时较长,可能意味着对象图复杂(引用链深),或SATB日志积压严重。

4.2 最终标记阶段的修正

text

复制代码
[GC remark, 0.0123 secs]
  • remark阶段(最终标记):就是STW处理SATB日志,将快照中记录的白色对象重新标记为灰色的过程。

  • 如果这个阶段耗时异常,通常意味着SATB日志过大,说明在并发标记期间有大量的引用被修改(如大量临时对象创建和销毁)。此时需要关注代码中是否存在"频繁更新引用"的热点逻辑。

4.3 清理阶段的"垃圾占比"

text

复制代码
[GC cleanup, 0.0011 secs]
  • 清理阶段会统计每个Region的垃圾占比,这个占比就是三色标记结果的直接体现:黑色对象占Region的比例越低,垃圾占比越高,越有可能被优先回收。

五、易混淆概念辨析:三色标记 ≠ GC算法

这是一个常见的认知误区,这里明确一下层次关系:

  • 三色标记 :是一种并发可达性分析算法 ,解决的是"如何在并发环境下准确标记存活对象"的问题。它是手段,不是目的。

  • CMS/G1/ZGC :是垃圾收集器 ,是完整的实现。它们都使用三色标记作为并发标记阶段的实现方式,但各自在"标记后的处理"(如CMS的并发清除、G1的复制整理、ZGC的染色指针)上完全不同。

类比:三色标记像"如何在地图上标注出所有需要保留的建筑"的通用方法,而CMS/G1/ZGC则是不同风格的"拆迁队",它们用这个方法标注,但后续怎么拆、怎么搬、怎么整理,各有各的流程。


六、总结:三色标记的"得与失"

补充一个原文未明说的本质权衡:

维度 传统STW标记 三色标记(并发)
正确性保证 绝对准确,无漏标/错标 通过屏障保证无漏标,但允许错标(浮动垃圾)
停顿时间 随堆增大线性增长,不可控 与堆大小无关,仅与GC Roots和SATB日志相关
吞吐量 高(无额外屏障开销) 略低(写屏障、日志记录消耗CPU)
内存占用 较高(RS、SATB日志、卡表等元数据)

核心权衡 :三色标记本质是用**"CPU资源(屏障开销) + 内存资源(元数据) + 浮动垃圾(空间换时间)"** 来换取**"STW停顿时间的可控与降低"**。

理解了这个底层权衡,你在做GC选型和调优时,就能更清晰地判断:当应用对延迟极度敏感(如交易系统、游戏服务)时,接受少量浮动垃圾和CPU开销来换取低停顿是值得的;而当应用对吞吐量要求极高(如离线批处理)、且能接受较长STW时,Parallel Scavenge可能是更合适的选择。

相关推荐
Jerry2 小时前
LeetCode 28. 找出字符串中第一个匹配项的下标
算法
Jerry4 小时前
LeetCode 459. 重复的子字符串
算法
海石6 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
海石6 小时前
【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快
算法·leetcode
Jerry8 小时前
LeetCode 151. 反转字符串中的单词
算法
a11177611 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜11 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai
Jerry11 小时前
LeetCode 541. 反转字符串 II
算法
Jerry11 小时前
LeetCode 344. 反转字符串
算法
搞科研的小刘选手12 小时前
【香港大学主办&IEEE出版】第六届计算机视觉、应用与算法国际学术会议(CVAA 2026)
算法·计算机视觉·应用·学术会议