JVM GC 并发机制与三色标记算法

java-concurrency 📚 learning jvm-gc-concurrent · jvm · gc · three-color · concurrent-gc · write-barrier · read-barrier · zgc · shenandoah

TL;DR(30 秒扫完)

  • 三色严格定义:白=初始未访问 / 灰=工作队列 / 黑=扫描完毕
  • 不变式:黑不能指白(否则漏回收);灰不能指黑(否则误回收)
  • 两大并发问题:对象消失(漏回收,黑指白)+ 对象复活(误回收,白指黑)
  • 两种修复:写屏障(修复黑指白,CMS/G1/ZGC 经典)+ 读屏障(修复白指黑,Shenandoah/ZGC 现代)
  • ZGC 用读屏障+多映射把 STW 压到 <1ms

关键结论

结论 A三色标记是可达性分析的并发实现,本质相同
结论 BGC 目标 = 遍历结束后没有白色对象指向存活对象(等价:没有白色对象)
结论 C写屏障修复黑指白,读屏障修复白指黑
结论 DZGC 从写屏障转向读屏障——避免每次赋值都触发,改用多映射实现"移动对象不改引用"
结论 EShenandoah 用写屏障+Brooks 转发(对象头存转发指针)做到 STW <10ms

完整讲解(费曼四步)

STEP 1 · 概念

三色标记算法是并发 GC 的核心。它给每个对象染三种颜色(白/灰/黑),表示 GC 访问进度。三色不变式(黑不指白、灰不指黑)保证并发下的正确性;违反不变式时用写屏障或读屏障修复。

STEP 2 · 大白话

类比:想象你在画地图。
- 白色:还没去过(可能是垃圾也可能是幸存者)
- 灰色:去过但没画完周围的路(还在扫描中)
- 黑色:去过且画完了周围所有路(确定存活)
扫描规则:从黑画到灰、从灰画到黑。结束时只剩白色的就是垃圾。
但并发时用户线程在偷改地图——你刚划掉的一条路(黑指白),用户又建了条新路(重新指白),你可能就漏标了幸存者。这就是内存安全问题。

STEP 3 · 底层

三色严格定义

颜色定义含义
白色从未被 GC 访问过可能是垃圾也可能是存活(初始状态)
灰色已访问,但子节点未全扫描工作队列,待扫描
黑色已访问,所有引用都扫描完毕确定存活

算法流程

1. 所有对象初始为白色
2. GC Roots 全部染黑
3. 取灰节点扫描子节点:白→灰入队;扫完→变黑
4. 工作队列为空时结束
5. 剩余白色对象 = 垃圾,回收
不变式(正确性条件):
  • 规则 1:黑不能指白(否则漏回收)
  • 规则 2:灰不能指黑(否则可能误回收)

两大并发内存安全问题

问题 1:对象消失(漏回收 / 黑指白)
初始状态:    黑A → 白B
第1步:      扫描黑A,把白B变灰
第2步:      扫描黑A完成,A→B 断链(用户删链)
第3步:      用户创建黑C 引用白B
第4步:      B 仍是白色(从未被访问)
第5步:      遍历结束,B 被误回收 ❌
违反规则 1(黑不能指白)。 问题 2:对象复活(误回收 / 白指黑)
初始状态:    黑A → 白B
第1步:      用户创建白C 引用白B
第2步:      扫描A,A→B 断链
第3步:      遍历C,把B变灰→变黑
第4步:      用户删除 A→B 引用,创建黑A 引用白D(D 被删)
           B 仍存活但被错误地认为可回收 ❌
违反规则 2(灰不能指黑)。

两种修复方案

修复方案屏障类型规则使用者
写屏障Write Barrier对象引用赋值时:新引用指向白 → 父对象重染灰CMS、G1、ZGC 经典模式
读屏障Read/Load Barrier读指针时:检查有效性,无效则重新标记为黑Shenandoah、ZGC 现代模式

ZGC 的现代方案

为什么 ZGC 从写屏障转向读屏障?
  • 写屏障每次赋值都触发,影响吞吐
  • 读屏障只在读取指针时检查,对代码侵入小
多映射(Multi-Mapping)原理:
  • 同一虚拟地址映射到多个物理地址
  • 移动对象时改映射而非改对象引用
  • 配合读屏障检查指针有效性
  • 效果:STW 只有 Initial Mark 和 Final Relocate 两个瞬间,都 <1ms

Shenandoah 的 Brooks 转发

  • 对象头存转发指针
  • 读屏障检查指针有效性时顺带完成对象移动
  • STW <10ms

与 GC 器对照

GC 器三色标记屏障特殊机制
CMS有写屏障(Card Table)早期经典方案
G1有写屏障(Remembered Set)Region 结构
ZGC 经典有写屏障—
ZGC 现代有读屏障多映射
Shenandoah有写屏障Brooks 转发

CMS 中的三色标记应用

  • Initial Mark(STW):把 GC Roots 染黑
  • Concurrent Mark:三色并发扫描(灰→黑、白→灰)
  • Remark(STW):靠写屏障追踪并发期间的引用变动

STEP 4 · 简化

一句话总结:三色 = 白(未访问)/ 灰(扫描中)/ 黑(扫完);不变式 = 黑不指白 + 灰不指黑;违反时用写屏障或读屏障修复。
记忆口诀:
  • 白色初始,灰色队列,黑色完成
  • 黑不指白防漏回收,灰不指黑防误回收
  • 写屏障管赋值,读屏障管读取
  • ZGC 现代:读屏障+多映射 → STW <1ms

常见误区

"初始颜色是黑色"
白色是初始状态,从未被 GC 访问
"标白后再回收"
白色对象本身就是垃圾(初始未被访问),不是"标白后再回收"
"灰色是不确定存活"
灰色是扫描中的中间态,不是"不确定"
"三色标记就是标记回收"
三色标记是可达性分析的并发实现,解决并发下的内存安全
"三色标记所有 GC 器都用"
SerialGC 不需要(不需要并发)

延伸追问

三色标记的"对象消失"具体怎么走通?
黑 X 引用白 Y → 扫描 X 完成断链 X→Y → 用户创建黑 Z 引用 Y → 若 Y 从未被访问保持白 → 被误回收
写屏障具体怎么处理?
写屏障在对象引用赋值时插入:如果新引用指向白对象,把父对象重染灰;HotSpot 中通过 Card Table 实现
为什么 ZGC 从写屏障转向读屏障?
写屏障频繁触发(每次赋值)影响吞吐;读屏障只在读取指针时检查,对代码侵入小;配合多映射实现"移动对象不改引用"
Shenandoah 的 Brooks 转发是什么?
对象头存转发指针,读屏障检查有效性时顺带完成对象移动;STW <10ms
三色标记和可达性分析的关系?
三色标记是可达性分析的并发实现;本质都是从 GC Roots 遍历,区别在是否并发、如何处理引用变动

速查表

三色定义: 白=初始未访问 / 灰=工作队列 / 黑=扫描完毕
不变式: 黑不指白(防漏回收)+ 灰不指黑(防误回收)
并发问题: 对象消失(漏回收)+ 对象复活(误回收)
写屏障: 修复黑指白(CMS / G1 / ZGC 经典)
读屏障: 修复白指黑(Shenandoah / ZGC 现代)
ZGC 现代: 读屏障 + 多映射 → STW <1ms
Shenandoah: 写屏障 + Brooks 转发 → STW <10ms

Anki 候选卡片

Q: 三色严格定义?
A: 白=初始未访问 / 灰=工作队列 / 黑=扫描完毕
Q: 三色不变式两条?
A: 黑不能指白 + 灰不能指黑
Q: 写屏障修复什么问题?
A: 修复"黑指白"(对象消失/漏回收)
Q: 读屏障修复什么问题?
A: 修复"白指黑"(对象复活/误回收)
Q: ZGC 现代版用什么屏障?
A: 读屏障 + 多映射,STW <1ms

关联题目

  • ⚠️ 《什么是三色标记算法?》— Round 1 Q8, ⭐(三色定义反了,未提内存安全)
  • 关联知识

    三色标记不变式:黑不指白(漏回收)、灰不指黑(误回收);写屏障修黑指白、读屏障修白指黑
    画地图:白=未去过、灰=去过没画完、黑=画完了;结束时只剩白色的就是垃圾
    ✦ 记 忆 口 诀 ✦
    白灰黑三色;黑不指白灰不指黑;写屏障修黑指白,读屏障修白指黑
    关键可视化
    三色严格定义
    flowchart TB
      C["三色标记"] --> W["白色<br/>从未被 GC 访问<br/>可能是垃圾或存活"]
      C --> G["灰色<br/>已访问但子节点未全扫描<br/>工作队列待扫描"]
      C --> B["黑色<br/>已访问所有引用扫描完毕<br/>确定存活"]
      B -.遍历.-> G
      G -.扫描.-> W
    两条不变式
    flowchart LR
      I["三色不变式"] --> R1["规则 1:黑不能指白<br/>否则漏回收"]
      I --> R2["规则 2:灰不能指黑<br/>否则误回收"]
      R1 -.违反时.-> F1["写屏障修复<br/>CMS/G1/ZGC 经典"]
      R2 -.违反时.-> F2["读屏障修复<br/>Shenandoah/ZGC 现代"]
    对象消失(黑指白)修复
    flowchart LR
      S1["初始:黑A→白B"] --> S2["扫描黑A,白B→灰"]
      S2 --> S3["扫描黑A完成,A→B 断链"]
      S3 --> S4["用户创建黑C→白B"]
      S4 --> S5["B 仍白(从未被访问)"]
      S5 --> S6["遍历结束,B 被误回收 ❌"]
      S3 -.写屏障.-> W["A→B 断链时<br/>把 A 染灰<br/>触发重新扫描"]
    ZGC 读屏障+多映射
    flowchart LR
      Z["ZGC 现代方案"] --> RB["读屏障<br/>修复白指黑"]
      Z --> M["多映射<br/>移动对象不改引用"]
      RB --> P["避免每次赋值都触发<br/>改用读取时修复"]
      M --> P2["对象移动后<br/>旧地址自动转发到新地址"]
      P --> S["STW < 1ms"]
      P2 --> S
    知识关系

    ⬆️ 前置(Prerequisite)

    jvm-gc-algorithms

    🔄 延伸(Extends)

    jvm-gc-select-tune

    ⚡ 对比(Contrast)

    GC 算法— GC 算法讲'怎么做'(标记-清除/复制/整理);三色标记讲'怎么并发做'——是 GC 算法的并发实现
    🎯 概念 📏 规则 ⚠️ 误区 🔍 追问 ✨ 口诀 共 0 张卡,点击翻面