深度解析三色标记算法:JVM 并发 GC 的核心底层逻辑

在 JVM 垃圾收集的核心技术中,三色标记算法是实现 "并发标记" 的基石 ------ 从 CMS GC 的并发标记阶段,到 G1 GC 的并发标记流程,再到 ZGC/Shenandoah GC 的极致低延迟设计,都离不开三色标记的支撑。但多数开发者仅停留在 "知道有这个算法" 的层面,不清楚其核心逻辑、解决的问题以及如何处理并发场景下的引用变动。

本文将从 "为什么需要三色标记" 入手,拆解算法核心原理、标记流程、关键问题(漏标 / 错标)及解决方案,结合 CMS/G1 的实战场景,帮你彻底搞懂这一 JVM GC 的底层核心算法。


一、为什么需要三色标记算法?

在理解三色标记前,我们先回顾一个核心矛盾:传统的可达性分析算法需要暂停所有用户线程(STW) 才能保证标记结果准确 ------ 如果标记过程中用户线程修改对象引用,会导致标记结果错误(比如把可达对象标记为不可达,造成内存泄漏;或把不可达对象标记为可达,造成内存浪费)。

但 STW 会导致服务停顿,尤其在大堆内存场景下,长时间 STW 无法满足低延迟需求。因此,JVM 需要一种能在用户线程运行时(并发)完成标记 的算法,三色标记算法应运而生。

核心目标

在不全程 STW 的前提下,准确标记出堆中所有可达对象,为后续的垃圾回收提供可靠依据。


二、三色标记算法的核心定义

三色标记算法将堆中的对象分为三种 "颜色",分别代表不同的标记状态,通过颜色的转换完成并发标记:

表格

颜色 状态定义 核心特征
白色 未被标记的对象 初始状态所有对象都是白色;标记结束后,白色对象即为 "无用对象"(待回收)
灰色 已被标记,但引用的子对象未完全标记 标记过程中的 "中间状态",GC 线程需要继续遍历其引用的子对象
黑色 已被标记,且所有引用的子对象都已标记 标记完成的 "最终状态",黑色对象是可达的(有用对象)

核心规则

  1. 初始时,所有对象为白色;
  2. GC Roots 直接引用的对象标记为灰色,加入 "标记队列";
  3. GC 线程从标记队列中取出灰色对象,将其标记为黑色,同时将其引用的子对象标记为灰色(若子对象为白色),加入标记队列;
  4. 重复步骤 3,直到标记队列为空;
  5. 标记结束后,所有白色对象判定为不可达,可回收。

三、三色标记算法的基础执行流程(串行版)

为了便于理解,我们先看无用户线程干扰 的串行版三色标记流程(STW 场景),这是并发版的基础:

示例场景

假设存在对象引用链:GC Roots → A → B → C,且堆中还有一个孤立对象 D(无任何引用)。

执行步骤

  1. 初始状态:所有对象(A、B、C、D)均为白色;
  2. 标记 GC Roots 直接引用:将 GC Roots 引用的 A 标记为灰色,加入标记队列;
  3. 遍历灰色对象 :
    • 取出 A,标记为黑色;将 A 引用的 B 标记为灰色,加入队列;
    • 取出 B,标记为黑色;将 B 引用的 C 标记为灰色,加入队列;
    • 取出 C,标记为黑色;C 无引用的子对象,队列清空;
  4. 标记结束:A、B、C 为黑色(可达),D 为白色(不可达),D 将被回收。

核心结论

串行版三色标记是 "可达性分析" 的可视化实现,逻辑简单且结果准确,但需要全程 STW------ 而我们真正需要的是并发版三色标记(允许用户线程运行时标记)。


四、并发版三色标记:核心问题与解决方案

当 GC 线程并发标记时,用户线程可能修改对象引用(比如 A 原本引用 B,现在改为引用 D),这会导致三色标记出现两种致命问题:

4.1 并发标记的核心问题

问题 1:漏标(丢失可达对象)

场景 :黑色对象 A 放弃引用白色对象 B,白色对象 C 引用 B,但 C 未被标记(灰色 / 白色)。后果:B 实际可达(被 C 引用),但标记结果为白色,会被错误回收,导致程序崩溃(内存泄漏 + 空指针)。

问题 2:错标(保留无用对象)

场景 :白色对象 B 原本无引用,用户线程让黑色对象 A 引用 B,但 B 未被标记。后果:B 实际可达,标记结果为白色,若未处理会被回收(漏标);或通过补救机制标记 B 为可达,但如果 B 本是无用对象,会被错误保留(内存浪费)。

注:漏标是 "致命错误"(会导致程序崩溃),错标是 "非致命错误"(仅内存浪费,下次 GC 可回收),因此三色标记的核心是解决漏标问题。

4.2 解决漏标:两种核心机制

JVM 通过两种机制解决并发标记的漏标问题,分别应用在 CMS/G1 和 ZGC 中:

机制 1:写屏障(Write Barrier)+ SATB(Snapshot At The Beginning)

核心思想 :以 "标记开始时的快照" 为基准,确保所有在标记开始时可达的对象最终都被标记为黑色。实现方式:

  1. 写屏障:拦截用户线程的 "引用修改操作"(如 A 放弃引用 B);
  2. 当黑色对象 A 放弃引用白色对象 B 时,写屏障将 B 记录到 "SATB 日志" 中;
  3. 并发标记结束后,GC 线程遍历 SATB 日志,将 B 重新标记为可达(灰色);
  4. 最终所有可达对象都会被标记为黑色,避免漏标。

适用场景 :CMS GC、G1 GC 的并发标记阶段。优点 :实现简单,能彻底解决漏标;缺点:可能标记部分已无用的对象(错标),产生 "浮动垃圾"(需下次 GC 回收)。

机制 2:写屏障 + 增量更新(Incremental Update)

核心思想 :关注 "引用的新增",确保新增的引用链被重新标记。实现方式:

  1. 写屏障:拦截用户线程的 "引用新增操作"(如黑色对象 A 引用白色对象 B);
  2. 当 A 引用 B 时,写屏障将 A 重新标记为灰色,加入标记队列;
  3. GC 线程重新遍历 A 的引用链,将 B 标记为可达;
  4. 确保新增引用的对象被标记,避免漏标。

适用场景 :部分版本的 G1 GC、早期的 CMS GC。优点 :仅处理新增引用,减少浮动垃圾;缺点:实现复杂,需要重新遍历已标记的黑色对象。

4.3 实战对比:SATB vs 增量更新

特性 SATB(快照式) 增量更新(增量式)
关注对象 被删除的引用 新增的引用
处理方式 记录被放弃的白色对象,后续重新标记 把新增引用的黑色对象变回灰色,重新遍历
浮动垃圾 较多(记录了快照后无用的对象) 较少(仅处理新增引用)
实现复杂度 低 高
适用收集器 CMS、G1(主流) 部分 G1 版本、早期 CMS

五、三色标记在主流 GC 中的实战应用

5.1 CMS GC 中的三色标记

CMS GC 的 "并发标记阶段" 完全基于三色标记算法,结合 SATB 机制:

  1. 初始标记(STW):标记 GC Roots 直接引用的对象(灰色),此时所有对象为白色 / 灰色;
  2. 并发标记(无 STW):GC 线程并发遍历灰色对象,标记为黑色,同时用户线程可修改引用;
  3. 写屏障拦截:用户线程修改引用时,SATB 日志记录被放弃的白色对象;
  4. 重新标记(STW):遍历 SATB 日志,修正漏标对象,将其标记为可达;
  5. 并发清除:回收白色对象(不可达)。

5.2 G1 GC 中的三色标记

G1 GC 的并发标记阶段同样基于三色标记 + SATB,且做了优化:

  1. 以 Region 为单位进行标记,而非全堆;
  2. 标记过程中计算每个 Region 的 "垃圾占比",为后续筛选回收做准备;
  3. 写屏障结合 "卡表(Card Table)" 和 "Remembered Set(RS)",仅记录跨 Region 的引用修改,减少日志量,提升效率。

5.3 ZGC 中的三色标记(进阶)

ZGC 采用 "染色指针" 技术,将对象的标记状态存储在指针的预留位中(而非对象头),实现了 "无 STW 的三色标记":

  1. 指针的不同位代表 "白色 / 灰色 / 黑色",标记时无需修改对象,直接修改指针;
  2. 结合读屏障 + 写屏障,全程无 STW,标记停顿时间控制在微秒级;
  3. 彻底解决漏标问题,且几乎无浮动垃圾。

六、三色标记算法的核心价值与局限性

6.1 核心价值

  1. 实现并发标记:打破了 "标记必须 STW" 的限制,大幅降低 GC 停顿时间;
  2. 保证标记准确性:通过写屏障 + SATB / 增量更新,避免漏标致命错误;
  3. 适配不同 GC 场景:从 CMS 到 G1 再到 ZGC,三色标记是并发 GC 的通用底层逻辑。

6.2 局限性

  1. 无法完全避免浮动垃圾:并发标记过程中产生的无用对象(白色)会被保留,需下次 GC 回收;
  2. 占用 CPU 资源:写屏障和日志记录会消耗额外 CPU,降低用户线程吞吐量;
  3. 依赖屏障技术:写屏障 / 读屏障的实现增加了 JVM 的复杂度,且不同平台(如 ARM/x86)的实现差异较大。

七、总结

三色标记算法是 JVM 并发 GC 的 "底层骨架",核心要点可总结为:

  1. 核心定义:通过白 / 灰 / 黑三色标记对象的可达状态,实现并发可达性分析;
  2. 核心问题:并发标记时用户线程修改引用会导致漏标(致命),通过写屏障 + SATB / 增量更新解决;
  3. 实战应用:CMS/G1 采用 SATB + 三色标记,ZGC 结合染色指针实现极致低延迟;
  4. 核心权衡:以少量浮动垃圾和 CPU 开销为代价,换取 STW 停顿时间的大幅降低。
相关推荐
aqiu1111111 小时前
【C++算法打怪专栏】8. 字符串转换整数 (atoi)
开发语言·c++·算法
代码什么用9 小时前
Spring基础使用
java·后端·spring
信奥卷王10 小时前
[GESP202609 六级] 分树规划
算法
Su米苏10 小时前
Spring AI 中MCP 与普通 @Tool的区别
java·人工智能·spring
夏幻灵10 小时前
LeetCode 21:合并两个有序链表——双指针与哑节点详解
算法·leetcode·链表
码云数智-园园10 小时前
unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析
java·开发语言
老板一杯拿铁10 小时前
Java 异常体系详解:从字节码原理到最佳实践
java
蜗牛互联网10 小时前
Java 17 HttpClient调用文件转写API的超时与失败回退
java·人工智能·后端
晒太羊的猫11 小时前
jvm面试题
jvm