ZGC 与 Shenandoah 低延迟收集器
更新时间:2026-09-18。当堆到几十上百 GB、又要求个位数毫秒以下停顿时,G1 也会力不从心——这就是 ZGC/Shenandoah 的战场。前置阅读:G1 收集器、G1 与 CMS 的区别。
一、要解决什么:停顿随堆变大
G1 回收时仍要 STW 复制存活对象,堆越大、单次回收的存活对象越多,停顿越长。ZGC/Shenandoah 的目标是:把对象的移动整理也做成与业务线程并发,使 GC 停顿做到:
- 亚毫秒~个位数毫秒;
- 与堆大小几乎无关(16GB 和 256GB 停顿差不多)。
二、两种实现思路
| 维度 | ZGC(Oracle) | Shenandoah(Red Hat) |
|---|---|---|
| 核心技术 | 染色指针(colored pointer) + 读屏障 + 负载值 | Brooks 转发指针 + 读屏障 |
| 标记/转移/重定位 | 几乎全程并发 | 几乎全程并发 |
| 停顿内容 | 仅根扫描等极短阶段 STW | 仅初始/最终标记等极短阶段 |
| JDK 状态 | JDK11 实验、JDK15 生产可用、JDK21 分代 ZGC | JDK12 实验、JDK15 生产可用(部分发行版) |
| 平台 | Linux/x64、ARM、macOS、Windows 逐步完善 | OpenJDK 多平台 |
ZGC 染色指针:在 64 位指针的高位存标记位(marked0/marked1/remap/finalizable),对象移动后通过读屏障 + 转发信息让访问者拿到新地址,几乎不需要停下来批量改引用。 Shenandoah Brooks 指针:每个对象多一个转发槽,移动时先放副本并改转发指针,读屏障保证读到最新副本。两者都靠读屏障换取并发整理。
三、与 G1 的取舍
| 指标 | G1 | ZGC/Shenandoah |
|---|---|---|
| 停顿 | 几十~两百 ms,随堆增大 | 亚毫秒~几 ms,基本不随堆变 |
| 吞吐 | 高(屏障开销小) | 略低(读屏障有开销,典型损失几个百分点~十几个点) |
| 内存开销 | RSet | 染色指针/转发指针 + 额外数据结构 |
| 适用 | 6GB+、200ms 停顿可接受的绝大多数服务 | 超大堆、超低延迟(交易、实时、大数据) |
bash
# JDK 15+ 开启
-XX:+UseZGN # ZGC(JDK21 默认分代;JDK11~20 为单代)
-XX:+UseShenandoahGC # Shenandoah(需对应 OpenJDK 构建)
-XX:+UnlockExperimentalVMOptions # JDK 早期版本需配合四、常见坑
- 为降停顿无脑上 ZGC 却吞吐暴跌:读屏障牺牲吞吐,若你的 SLA 用 G1 已满足(如 P99 < 200ms),没必要换;低延迟收集器是为「停顿敏感 + 大堆」准备的。
- JDK 版本/构建不支持:Shenandoah 不是所有发行版都内置,ZGC 在 JDK15 才生产可用、JDK21 才有分代 ZGC(大幅降低分配停顿),老版本要谨慎。
- 以为「零停顿」:仍有极短 STW(根扫描、初始标记)和安全点停顿,且线程数过多时根扫描也会变长。
- 堆外内存/监控没跟上:换收集器后 GC 日志、监控指标口径变化,要重新接观测工具,不能沿用 G1 的解读。
分代与停顿基础见 JVM 运行时内存与堆分代;频繁 Full GC 排查见 Full GC 频繁怎么排查。
总结
ZGC 和 Shenandoah 都把对象移动整理做成与业务并发,分别靠染色指针+读屏障(ZGC)和 Brooks 转发指针+读屏障(Shenandoah),把 GC 停顿压到亚毫秒级且几乎不随堆大小增长,代价是读屏障带来几个百分点以上的吞吐损失和更高内存/版本要求;它们不是 G1 的全面替代——G1 满足停顿目标时吞吐更高、生态最稳,只有在超大堆且对个位数毫秒停顿有硬要求时(实时交易、大内存数据系统,JDK15+/21 分代 ZGC)才值得换,且换后要重新对接监控并接受吞吐折让。