背景:为下半年的 JDK 21 LTS 提前做验证
四月我们开始评估下半年升级到 JDK 21 LTS 的可行性。GC 是这次升级最敏感的部分——ZGC 在 JDK 21 里终于要兑现「亚毫秒停顿」的承诺,Shenandoah 也迭代到第三代。我拉了 JDK 21 的早期 EA build(当时到 build 19 阶段),在一台 8C16G 的压测机上,用真实订单服务的堆快照,把 G1、ZGC、Shenandoah 三种收集器跑了一遍对比。先说清楚:以下数据是 EA 阶段的预研,不是 GA 后的生产结论。
测试场景与配置
堆固定 12GB,用 JMeter 模拟下单混合负载(读写比 7:3),持续 30 分钟,观察吞吐、停顿、内存开销:
# G1(JDK 21 默认)
-XX:+UseG1GC -Xmx12g -Xms12g
# ZGC
-XX:+UseZGC -Xmx12g -Xms12g
# Shenandoah
-XX:+UseShenandoahGC -Xmx12g -Xms12g
吞吐对比:G1 仍是最稳的
| 收集器 | 吞吐(事务/秒) | 最大停顿 | 平均停顿 | 常驻内存开销 |
|---|---|---|---|---|
| G1 | 41800 | 210ms | 28ms | 12.0GB |
| ZGC | 38900 | 1.4ms | 0.6ms | 14.8GB |
| Shenandoah | 40100 | 3.2ms | 1.1ms | 13.6GB |
ZGC 吞吐比 G1 低约 7%,Shenandoah 低约 4%。代价换来了数量级的停顿下降。G1 在 12GB 堆下最大停顿仍有 210ms,是 Promotion Failure 触发的 Full GC 边缘。
延迟:低延迟场景 ZGC 完胜
我们的支付回调链路对 P99 敏感。ZGC 的 P99 停顿稳定在 1.4ms 以内,Shenandoah 差不多量级。G1 虽然平均 28ms,但偶发会冲到 200ms+,在延迟敏感的接口上会直接变成超时。
ZGC 的秘密是染色指针 + 读屏障,几乎所有回收工作都和应用线程并发,不依赖分代。Shenandoah 用 Brooks 指针 + 写屏障,原理不同但目标一致。
内存开销的隐性成本
ZGC 为了染色指针要预留一部分地址空间,且需要更多内存来容纳并发阶段的浮动垃圾,实际常驻比 G1 多 2~3GB。12GB 堆场景下差异不明显,但如果是 64GB 大堆,ZGC 的额外开销会更可观。Shenandoah 居中。
选型结论
- 堆 < 16GB、吞吐优先、能容忍百毫秒级停顿:G1 足够,且最省心。
- 低延迟交易链路、堆中等:ZGC 的亚毫秒停顿值得那 7% 吞吐损失,前提是机器内存有裕量。
- 想折中、又不想给 ZGC 那么多内存:Shenandoah 是稳妥选择。
写在后面
现在回头看,《JDK 21 生产环境 GC 选型:G1、ZGC、Shenandoah 实测》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。