ZGC 都上了,GC 调参这件事是不是可以彻底退休了?
帖子正文与楼层回复。
背景
做了 10 年性能优化,有一个观察越来越强烈:GC 调参这件事,正在从"必修课"变成"选修课",甚至有人觉得可以直接"退役"。
我最近带团队把一个 4C8G 的 Java 服务从 JDK 11(G1,-Xmx4g)迁到 JDK 21(ZGC generational,-Xmx4g),代码、配置、流量全不动,只换 GC 收集器。结果:
- G1:P50 = 15ms,P99 = 45ms,Full GC 每周 1~2 次,每次停顿 200~400ms。
- ZGC generational:P50 = 14ms,P99 = 42ms,0 次 Full GC,max pause < 1ms。
P50/P99 的平均值差距不大,但P99 的"毛刺"彻底消失了——以前每周 1~2 次的 200~400ms 停顿,现在完全没了。业务方的原话是"体感变稳了"。
这让我开始认真思考一个问题:如果 ZGC / Shenandoah 把 pause 压到了 < 1ms,那过去我们花大量时间调 -Xmx、-XX:MaxGCPauseMillis、-XX:InitiatingHeapOccupancyPercent 这些事,是不是可以彻底不做了?
常见分歧 / 几种做法
团队里(以及我见过的其他团队)至少有三种立场:
-
"GC 调参已死":ZGC / Shenandoah 是 low-pause 收集器,pause 时间跟堆大小无关(< 1ms),你只需要保证 -Xmx 合理,剩下的交给 GC。G1 / CMS 时代的调参经验(IHOP、PauseTarget)对 ZGC 没有意义。 → 我的反驳:ZGC 的 concurrent mark 和 relocation 仍然吃 CPU 和内存带宽,堆越大这些开销越明显,只是 pause 不变。"pause 小"不等于"开销小"。
-
"GC 调参只是换了个维度":从"调 pause"变成"调 CPU 开销"和"调堆大小"。ZGC 的 concurrent 线程数、内存占用(堆的 1.3~1.5 倍)、relocation 频率,这些仍然需要关注。 → 我认为这是最务实的立场。
-
"GC 调参永远不死":JVM 不是万能的。内存泄漏、对象分配速率、GC 与业务线程的 CPU 竞争、容器环境下的 -Xmx 与 cgroup 的交互——这些都不是换 GC 就能解决的。 → 我部分认同:这些更准确地说是"内存管理"问题,不完全是"GC 调参"。
我的倾向与理由
我倾向于第 2 种立场,但加一个前提:
GC 调参没有死,但它的"对象"变了。
过去我们调的是:
- -Xmx(堆大小)
- -XX:MaxGCPauseMillis(pause 目标)
- -XX:InitiatingHeapOccupancyPercent(IHOP)
- -XX:ConcGCThreads(并发线程数)
现在(ZGC / Shenandoah 时代)我们该调的是:
- -Xmx(堆大小,仍然重要,但上限更宽松)
- GC 的 CPU 开销:ZGC 的 concurrent mark 和 relocation 吃 CPU,跟堆大小近似成正比。4C 机器上 -Xmx4g 的 ZGC,concurrent 阶段可能吃掉 1~2 核的 CPU——这不是"pause",是"税"。
- 内存占用:ZGC 需要堆的 1.3~1.5 倍物理内存(off-heap 的 mapping 空间),容器环境要算清楚,否则 OOMKilled。
- 对象分配速率:这个跟 GC 选择无关,是代码问题。分配速率 > 500MB/s 的服务,换 GC 的收益会明显打折。
具体做法(我团队现在的 checklist):
- 上 ZGC 之前,先量对象分配速率(
jstat -gcutil <pid> 1000 60看 YGC 频率 + async-profiler 的 alloc 模式看 top 分配点)。分配速率 > 500MB/s 的,先优化代码再换 GC。 - 上 ZGC 之后,重点监控 CPU 利用率和RSS 内存,而不是 pause time。pause 已经不是瓶颈了。
- 容器环境:-Xmx 不要超过 cgroup memory limit 的 70%(ZGC 需要额外 30~50% 的 off-heap mapping 空间 + 堆外内存 + 线程栈)。
- 保留 GC 日志(
-Xlog:gc*,gc+heap=debug:file=gc.log),不是为了"调参",是为了快速排除 GC 不是根因——很多时候 P99 毛刺根本不是 GC,是下游、是 IO、是锁竞争。GC 日志是你"排除法"的第一块拼图。
想听大家在什么场景下怎么选
- 你们团队现在主力用什么 GC?G1?ZGC?Shenandoah?Epsilon?什么 JDK 版本?
- 换 GC 之后,CPU 利用率变化了多少?有没有人认真算过 ZGC 的"隐性 CPU 税"?(我见过 4C 机器上 ZGC 比 G1 多耗 1.5 核 CPU 的案例)
- 容器环境下,-Xmx 和 cgroup memory limit 你们怎么定的?有没有踩过"ZGC 映射空间不够 → OOMKilled"的坑?
- 一个更尖锐的问题:如果 ZGC 把 pause 压到 < 1ms,但 P99 还是毛刺,那"GC 不是根因"这个结论怎么快速证明?你们有没有一套"排除 GC"的标准流程(比如看哪些指标、跑什么命令)?
- 最后一个开放问题:对于"短生命周期、低延迟"的服务(比如 gRPC sidecar、API Gateway),你们会用 Epsilon(不 GC)还是 ZGC?Epsilon 的内存泄漏风险怎么管?
欢迎带真实数据来聊:JDK 版本、GC 类型、-Xmx、CPU 核数、P50/P99 数字、GC 日志片段。空谈方法论的咱们另开一帖。