跳过导航

G1、ZGC、Shenandoah 应该如何选择?从延迟目标到生产验证

约 7 分钟...次浏览
专栏JVM 与 Java 核心第 9 篇

垃圾收集器选型不是“谁更新就用谁”,也不是背一张吞吐量与暂停时间对比表。真正的问题是:在给定堆大小、对象分配速率、存活集、CPU 配额和延迟 SLO 下,哪一种收集器能以可接受的资源成本稳定达标。

1. 先把业务目标翻译成 GC 指标

选型前至少确认:最大堆、常态与峰值分配速率、对象存活率、可用 CPU、容器内存上限、允许的 p99/p999 暂停,以及吞吐下降能否接受。

例如“接口不能卡”无法测试,下面的目标才可以:

堆:16 GiB
峰值分配速率:2.5 GiB/s
业务 p99:150 ms
GC 单次暂停 p99:小于 20 ms
CPU 配额:8 核
峰值持续时间:15 分钟

GC 日志中的暂停并不等于请求延迟,但长暂停会直接推高尾延迟。应用锁竞争、数据库、Safepoint 到达耗时也会制造类似现象,因此不能只看平均 GC 时间。

2. G1:分区化的代际收集器

G1 把堆切成多个 Region,优先回收收益较高的区域。年轻代回收会 Stop-The-World,老年代标记大部分并发进行,之后通过 Mixed GC 同时回收年轻代和部分老年代。

它的优势是成熟、代际假设有效、吞吐表现通常较好,并能通过暂停目标影响每轮回收工作量:

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags

MaxGCPauseMillis 是软目标,不是承诺。对象复制量过大、巨型对象、引用处理、记忆集更新或 evacuation failure 都可能突破目标。若堆余量太小,并发标记来不及完成,G1 甚至可能退化到代价很高的 Full GC。

G1 通常适合数 GB 到数十 GB 堆、吞吐与延迟都重要、暂停容忍度在几十到数百毫秒的通用服务。

3. ZGC:把绝大多数重工作移出暂停

ZGC 通过有色指针、加载屏障和并发转移等机制,让标记、重定位等主要工作与应用线程并发进行。它的核心价值是暂停时间通常不会随堆大小线性增长,适合大堆和严格尾延迟目标。

版本演进必须说清楚:JDK 21 提供分代 ZGC,但需要显式开启;JDK 23 将分代模式设为默认,JDK 24 移除了非分代 ZGC。因此 Java 21 与 Java 25 的参数和日志不能混着解释。JDK 25 上通常只需:

-XX:+UseZGC
-Xlog:gc*,safepoint:file=gc-zgc.log:time,uptime,level,tags

低暂停并不免费。并发回收需要 CPU,屏障会产生运行时开销,且必须保留足够堆余量,让回收速度追上分配速度。当容器 CPU 被严格限流、分配速率极高时,ZGC 可能出现 allocation stall;这时增加堆只是延后问题,仍要减少分配或增加回收资源。

ZGC 更适合数十 GB 甚至更大堆、要求毫秒级暂停、愿意用部分吞吐和资源换取尾延迟稳定性的服务。

4. Shenandoah:并发压缩的另一条路线

Shenandoah 同样将对象转移与引用更新并发化,目标也是让暂停与堆大小弱相关。它使用转发指针与读/写屏障完成并发疏散,并提供不同启发式策略。

-XX:+UseShenandoahGC
-Xlog:gc*,safepoint:file=gc-shenandoah.log:time,uptime,level,tags

其可用性取决于具体 JDK 发行版:不是每个厂商或构建都包含 Shenandoah。JDK 25 引入了实验性的分代 Shenandoah,不能把它与成熟的非分代模式或某个发行版的回移植版本混为一谈;实验能力进入生产前还需检查供应商支持承诺。选择它之前要确认生产 JDK、长期支持策略、容器镜像以及诊断工具链一致。

5. 不要只凭收集器标签做判断

维度G1ZGCShenandoah
主要定位吞吐与可预测暂停平衡极低暂停、大堆极低暂停、大堆
代际能力成熟JDK 23+ 默认,JDK 24+ 仅分代JDK 25 提供实验性分代模式,发行版可能不同
并发转移否,疏散阶段暂停
CPU/屏障代价相对较低较明显较明显
发行版覆盖广主流 OpenJDK 可用需确认发行版
常见风险长 Young/Mixed GC、退化 Full GCallocation stall、CPU 不足degenerated/full GC、CPU 不足

表格只能用来提出候选。真实结果还受 JDK 版本、对象图、NUMA、容器限额和应用代码影响。

6. 设计公平的选型压测

压测必须固定应用版本、JDK、机器、流量模型和堆大小,仅替换收集器及必要参数。至少覆盖:

  1. 常态流量足够长时间运行;
  2. 超过预期峰值的持续压力;
  3. 缓存预热和冷启动;
  4. 批任务、全量查询、大报文等最坏路径;
  5. 容器 CPU 限流和实例滚动发布;
  6. 故障恢复后的流量回灌。

同时记录业务 p50/p99/p999、吞吐、错误率、CPU、RSS、分配速率、GC 暂停分位数、并发周期、GC 后存活集和 Safepoint。不要用几分钟的平均值掩盖周期性退化。

7. 常见错误调优

  • 为追求“零 GC”把堆设得极大,结果故障现场 dump 困难、容器成本上升。
  • 看到 G1 长暂停就无限降低 MaxGCPauseMillis,导致回收更频繁且吞吐下降。
  • 使用 ZGC 后只看暂停,不看 CPU、allocation stall 与业务吞吐。
  • 从旧博客复制大量实验性参数,忽略参数在新 JDK 中已废弃或语义改变。
  • 堆上限贴近容器内存上限,未给 Metaspace、线程栈、直接内存和本地库留空间。

8. 一个实用决策路径

如果当前 G1 已满足 SLO,就没有必要仅为“先进”切换。若主要问题是大堆下偶发长暂停,先确认不是内存泄漏、巨型对象或 CPU 饥饿;修复应用问题后,再把 ZGC 或 Shenandoah 纳入同机压测。

若延迟目标在几十到数百毫秒、吞吐和成本更重要,优先从 G1 开始。若堆很大且暂停必须稳定在毫秒级,优先验证 ZGC;已有特定发行版和 Shenandoah 经验的团队也可将其作为候选。

9. 上线检查清单

  • 明确业务延迟 SLO,而非只定义 GC 目标。
  • 使用生产相同的 JDK 发行版与容器限制压测。
  • 给并发收集器预留 CPU 和足够堆余量。
  • 监控暂停分位数、分配速率、存活集和退化事件。
  • 灰度期间保留同流量的旧收集器实例作为对照。
  • 准备快速回滚参数,并保留完整 GC 日志。

收集器没有绝对排名。G1、ZGC 和 Shenandoah 分别代表不同的资源交换方式:用暂停换吞吐,或用 CPU 与屏障成本换低暂停。可靠选型的终点不是参数表,而是生产流量模型下可重复的证据。

分享:
文章作者:狼码纪
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。文章可能参考了其他优秀文章,如有侵权请联系删除。