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:
移动对象期间,所有引用必须被更新——这必须 STW,否则应用线程可能读到旧地址(对象已被移走)。
这就是 G1 停顿时间有下限的根本原因:标记可以并发,但移动对象不能并发。
ZGC 的核心突破正是:让移动也并发。
ZGC 的核心创新
染色指针(Colored Pointer)
ZGC 把元数据存储在指针本身,利用 64 位地址空间的高位存储额外信息:
通过指针中的标记位,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 缓冲区仍有未处理的引用变化, 需要短暂暂停来确保没有对象被漏标
阶段二:并发转移准备(Concurrent Prepare for Relocation)
④ 并发选择 RelocationSet(与应用并发):
- 确定哪些 Region 要被转移(类似 G1 选择回收 Region)
- 构建转发表(Forwarding Table):记录旧地址 → 新地址映射
阶段三:并发转移(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 不用染色指针,而是在每个对象头前插入一个额外的间接指针:
优点: 不依赖 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) |
选择建议:
- 普通 Java 应用(几 GB 堆)→ G1(成熟稳定)
- 延迟敏感(< 10ms)+ 大堆(> 16GB)→ ZGC
- 延迟敏感 + 需要 32 位支持 → Shenandoah
ZGC 的演进
分代 ZGC(JDK 21,实验性)
原始 ZGC 是非分代的——不区分年轻代老年代,整个堆作为一个整体处理。
问题:短生命周期对象被反复扫描,浪费 CPU。
JDK 21 引入分代 ZGC,结合了分代假说的高效性和 ZGC 的低延迟:
-XX:+UseZGC -XX:+ZGenerational # 开启分代 ZGC
效果:吞吐量提升 10%~40%,停顿时间进一步降低。
参考资料
评论 (0)
发表评论