深度解析三色标记算法: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 停顿时间的大幅降低。
相关推荐
大写的老王2 小时前
OpenClaw 部署实战:一周完成 PHP 到 Java 的项目迁移
java·php·ai编程
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章40-特征找图
图像处理·人工智能·opencv·算法·计算机视觉
wearegogog1232 小时前
毫米波MIMO系统仿真中混合预编码的交替最小化算法
算法·预编码算法
油泼辣子多加2 小时前
【DL】Transformer算法应用
人工智能·深度学习·算法·机器学习·transformer
2301_795741792 小时前
C++中的代理模式变体
开发语言·c++·算法
hnlgzb2 小时前
Gemini:kotlin这几个类型有什么区别?类比java的文件,是怎样的?
java·开发语言·kotlin
fof9202 小时前
Base LLM | 从 NLP 到 LLM 的算法全栈教程 第二天
人工智能·算法·自然语言处理
2301_789015622 小时前
封装RBTree(红黑树)实现myset和mymap
开发语言·数据结构·c++·算法·r-tree
温酒斟与你2 小时前
idea编辑器新版UI回归旧版
java·ide·intellij-idea