三色标记算法

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

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

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可能是更合适的选择。

相关推荐
小O的算法实验室2 小时前
2026年AST SCI1区TOP,基于速度障碍法的多无人机三维避障策略,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
U-52184F693 小时前
深入理解“隐式共享”与“写时复制”:从性能魔法到内存深坑
java·数据库·算法
pp起床3 小时前
Part02:基本概念以及基本要素
大数据·人工智能·算法
lzh200409193 小时前
红黑树详解
算法
迈巴赫车主3 小时前
蓝桥杯20560逃离高塔
java·开发语言·数据结构·算法·职场和发展·蓝桥杯
泯仲3 小时前
Ragent项目7种设计模式深度解析:从源码看设计模式落地实践
java·算法·设计模式·agent
dulu~dulu3 小时前
算法---寻找和为K的子数组
笔记·python·算法·leetcode
moonsea02034 小时前
【无标题】
算法
佑白雪乐4 小时前
<ACM进度212题>[2026-3-1,2026-3-26]
算法·leetcode