专栏文章
专栏文章
GC 垃圾回收系列
1. GC 系列 #01:GC 总览与历史:自动内存管理的起源与演化 2. GC 系列 #02:GC 基础算法:标记-清除、标记-整理、复制收集、引用计数 3. GC 系列 #03:分代假说与分代收集:朝生夕死的工程哲学 4. GC 系列 #04:CMS 收集器:Java 第一个并发收集器的设计与缺陷 5. GC 系列 #05:G1 收集器:Region 化堆与可预测停顿 6. GC 系列 #06:ZGC 与 Shenandoah:亚毫秒级停顿的并发移动收集器 7. GC 系列 #07:Go 的 GC:并发三色标记与混合写屏障 8. GC 系列 #08:Python 的内存管理:引用计数与分代循环 GC 9. GC 系列 #09:GC 调优实践:JVM 日志解读与 G1/ZGC 参数策略

GC 系列 #06:ZGC 与 Shenandoah:亚毫秒级停顿的并发移动收集器

发布于 2026-05-25 14:16 👁 18 次阅读
#GC#JVM#理论#ZGC#Shenandoah

ZGC 和 Shenandoah 代表了 JVM GC 技术的最新水平,将 STW 停顿压缩到亚毫秒级。两者的核心创新在于把对象移动操作也并发化,而这正是 G1 无法解决的问题。


目录

章节 说明
为什么 G1 的停顿有下限 对象移动必须 STW 的根因
ZGC 的核心创新 染色指针 + 读屏障
ZGC 的工作流程 三色标记的并发实现
Shenandoah 的方案 Brooks 指针 vs 染色指针
ZGC vs G1 vs Shenandoah 适用场景对比
ZGC 的演进 分代 ZGC(JDK 21)

为什么 G1 的停顿有下限

G1 在 Mixed GC 时,将存活对象从回收 Region 复制到新 Region:

gc g1 object move

移动对象期间,所有引用必须被更新——这必须 STW,否则应用线程可能读到旧地址(对象已被移走)。

这就是 G1 停顿时间有下限的根本原因:标记可以并发,但移动对象不能并发

ZGC 的核心突破正是:让移动也并发


ZGC 的核心创新

染色指针(Colored Pointer)

ZGC 把元数据存储在指针本身,利用 64 位地址空间的高位存储额外信息:

gc zgc colored pointer

通过指针中的标记位,GC 可以在不扫描对象体的情况下判断对象状态,效率极高。

读屏障(Load Barrier)

当应用线程读取一个对象引用时,ZGC 插入读屏障检查指针的染色位:

// 应用代码
Object obj = someField.value;

// JIT 编译后(ZGC 读屏障)
Object obj = someField.value;
if (obj != null && !obj.isRemapped()) {   // 检查 R 位
    obj = remap(obj);   // 更新引用到对象新地址
    someField.value = obj;   // 自愈:顺手更新字段
}

关键思想: 不是 GC 集中时间更新所有引用,而是每次读取时懒更新,把引用更新的开销分摊到所有读操作上。

这被称为自愈(Self-Healing)——引用被读到时自动修复指向最新地址。


ZGC 的工作流程

ZGC 的 GC 周期分为三大阶段,每阶段只有极短的 STW:

阶段一:并发标记(Concurrent Mark)

标记开始(STW,~0.05ms):快照 GC Roots,设置标记起始点 STW 原因:需要一致的 GC Roots 快照,与 CMS/G1 初始标记相同

并发标记(与应用并发):遍历对象图,在指针的标记位(M0/M1)上打标; 不标记对象头,直接标记指针(染色指针的优势)

标记结束(STW,~0.05ms):处理最终的引用变化 STW 原因:并发标记结束瞬间,SATB 缓冲区仍有未处理的引用变化, 需要短暂暂停来确保没有对象被漏标

gc zgc satb buffer

阶段二:并发转移准备(Concurrent Prepare for Relocation)

并发选择 RelocationSet(与应用并发):

阶段三:并发转移(Concurrent Relocation)

转移开始(STW,~0.05ms):转移 GC Roots 直接引用的对象(第一层) STW 原因:GC Roots 指针必须原子性地更新到新地址, 如果应用线程同时访问这些 GC Roots,会读到旧地址,而旧对象正在被移走

并发转移(与应用并发):将 RelocationSet 中的存活对象复制到新 Region, 维护转发表(旧地址 → 新地址)

应用线程访问旧地址时:读屏障检测到指针未重映射(R 位未设置) → 通过转发表找到新地址 → 更新引用,设置 R 位(自愈)

完整停顿时间: 三次 STW 各约 0.05ms,总停顿 < 1ms(与堆大小无关!)


Shenandoah 的方案

Shenandoah(Red Hat 开发)与 ZGC 目标相同,但技术路线不同。

Brooks 指针(Forwarding Pointer)

Shenandoah 不用染色指针,而是在每个对象头前插入一个额外的间接指针

gc shenandoah brooks ptr

优点: 不依赖 64 位指针的高位,32 位系统也可用 缺点: 每个对象多 8 字节开销,每次访问多一次间接寻址

读/写屏障 vs 仅读屏障

ZGC Shenandoah
并发移动机制 染色指针 + 读屏障 Brooks 指针 + 读/写屏障
对象头开销 无(利用指针高位) +8 字节/对象
架构依赖 需要 64 位 无限制
屏障开销 仅读屏障 读+写屏障(略重)

ZGC vs G1 vs Shenandoah

维度 G1(JDK 9 默认) ZGC(JDK 15+) Shenandoah
最大停顿 100ms~1s < 1ms < 10ms
吞吐量 略低(读屏障开销) 略低
堆大小 几GB~几十GB 几GB~16TB 几GB~几TB
停顿是否与堆大小相关 否(核心优势)
适用场景 通用(大多数应用) 超大堆、延迟敏感 延迟敏感
JDK 版本 JDK 9+ 默认 JDK 15 正式 JDK 12(非 Oracle JDK)

选择建议:


ZGC 的演进

分代 ZGC(JDK 21,实验性)

原始 ZGC 是非分代的——不区分年轻代老年代,整个堆作为一个整体处理。

问题:短生命周期对象被反复扫描,浪费 CPU。

JDK 21 引入分代 ZGC,结合了分代假说的高效性和 ZGC 的低延迟:

-XX:+UseZGC -XX:+ZGenerational   # 开启分代 ZGC

效果:吞吐量提升 10%~40%,停顿时间进一步降低。

参考资料

  • Per Liden & Stefan Karlsson:"ZGC - The Z Garbage Collector"(JDK Summit 演讲)
  • Christine Flood:"Shenandoah: The Garbage Collector That Could"
  • JEP 333: ZGC: A Scalable Low-Latency GC
  • JEP 439: Generational ZGC
  • G1 收集器
  • Go 的 GC
← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论