Java 的 G1、ZGC 和 Shenandoah 该怎么选?
按 JDK 版本、堆大小、停顿目标和 CPU 核数四步选型:小堆低延迟不敏感用 Parallel,100~300ms 停顿选 G1,个位数毫秒选 ZGC 或 Shenandoah,核少优先 G1,并给出参数配置与 GC 日志验证。
学完这篇,你能按「JDK 版本 → 堆大小 → 停顿目标 → CPU 核数」四步走,在 G1、ZGC、Shenandoah 之间做出有依据的选择,并用 GC 日志验证效果。
第一步:先确认 JDK 版本,它决定哪些 GC 能用
这一步要拿到两个信息:JDK 版本号和默认 GC,后面的所有选择都建立在这上面。在服务器上执行:
java -version
java -XX:+PrintFlagsFinal -version | grep -E "UseG1GC|UseZGC|UseShenandoahGC"
可选项对照如下,这是选型的硬前提:
| JDK 版本 | 可用 GC |
|---|---|
| JDK 8 | 只能用 G1(需 -XX:+UseG1GC 显式开启),ZGC/Shenandoah 都没有 |
| JDK 11 | ZGC 作为实验特性加入,需加 -XX:+UnlockExperimentalVMOptions |
| JDK 15 | ZGC、Shenandoah 转为正式特性,不再需要 Unlock 参数 |
| JDK 21 | 新增分代 ZGC,需 -XX:+UnlockExperimentalVMOptions -XX:+ZGenerational |
| JDK 23 及以上 | 分代 ZGC 成为默认,直接 -XX:+UseZGC 就行 |
注意:JDK 9 起 G1 是默认 GC,所以你什么都不加跑的就是 G1。如果你的 JDK 是 8,就不要考虑 ZGC 和 Shenandoah 了。
第二步:按堆大小和停顿目标初筛
这一步是用三个问题把范围缩到一个。按顺序问自己:
- 堆有多大? 堆小于 4GB 且对延迟不敏感(比如批处理),直接用
-XX:+UseParallelGC就行,吞吐最高,不必上 G1。 - 能接受多长的停顿? 要求 100~300ms 级别、堆在 4GB~32GB,选 G1。要求个位数毫秒、且停顿不能随堆增长而变长,选 ZGC 或 Shenandoah。
- CPU 核数够吗? ZGC 和 Shenandoah 靠并发线程和应用抢 CPU,核数少于 4 时吞吐掉得明显,这时候 G1 反而更稳。
一句话总结:**G1 是分代回收,停顿可预测但随堆变大而变长;ZGC 和 Shenandoah 是并发整理,停顿基本与堆大小无关。**
第三步:G1 的参数怎么设
这一步得到一组能直接上生产、又不会调坏的 G1 参数。G1 调优只调两个值:
java -Xms16g -Xmx16g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:InitiatingHeapOccupancyPercent=45 \
-Xlog:gc*:file=gc.log:time,uptime,level,tags \
-jar app.jar
MaxGCPauseMillis=200是停顿目标,G1 会据此调整年轻代大小。InitiatingHeapOccupancyPercent=45控制何时启动并发标记,默认 45,如果频繁出现 Full GC 可以调低到 35。
注意:不要把
-Xms和-Xmx设成不一样的值。堆反复伸缩会让 G1 的停顿预测失效,这是最常见的「调了没效果」原因。
第四步:ZGC 怎么开,有哪些坑
这一步得到一条 ZGC 启动命令。JDK 17 及以上直接写:
java -Xms32g -Xmx32g \
-XX:+UseZGC \
-Xlog:gc*:file=gc.log:time,uptime,level,tags \
-jar app.jar
ZGC 的目标是停顿不超过 1ms,且不随堆大小变化,TB 级堆也能扛。代价有两个:
注意:ZGC 会额外消耗 CPU(染色指针 + 读屏障),实测吞吐一般比 G1 低 5%~15%。如果你的应用是计算密集型、CPU 已经跑满 80%,上 ZGC 可能得不偿失。
注意:ZGC 在小堆(2GB 以下)上没有优势,反而因并发线程占用内存而更吃亏。堆小于 8GB 时优先考虑 G1。
第五步:Shenandoah 怎么开
这一步得到 Shenandoah 的启动命令,并确认你的 JDK 里有它。Shenandoah 由 Red Hat 主导,JDK 12 起进入 OpenJDK 主线,JDK 15 转正:
java -Xms32g -Xmx32g \
-XX:+UseShenandoahGC \
-Xlog:gc*:file=gc.log:time,uptime,level,tags \
-jar app.jar
Oracle JDK 的官方构建里不含 Shenandoah。要用它,请换成 Eclipse Temurin、Amazon Corretto、Azul Zulu 或 Red Hat 的 OpenJDK 构建。先用第一步的 PrintFlagsFinal 命令确认 UseShenandoahGC 这个 flag 存在,不存在就是发行版不支持。
Shenandoah 和 ZGC 定位接近,区别是:Shenandoah 的内存开销通常更小,适合几十 GB 到几百 GB 的堆;ZGC 在超大堆上更占优。
第六步:用 GC 日志实测对比
这一步是把选型从「猜」变成「量」。用同一份压测流量,分别跑 G1 和候选 GC,然后看 gc.log:
- 搜
Pause行,看最大停顿(Pause Young和Pause Full分开看)。 - 看 Full GC 有没有发生,发生了几次。
- 对比同一压测下的 QPS 和 P99 延迟。
只有 P99 延迟达标、且吞吐损失能接受的那个,才是最终答案。别只看「GC 停顿变小了」就下结论。
小结
- JDK 8 只能用 G1;ZGC 需 JDK 11+,Shenandoah 需 JDK 12+,两者 JDK 15 起转正。
- 堆 < 4GB 用 ParallelGC;4GB~32GB 首选 G1;要求毫秒级停顿或堆很大,才上 ZGC/Shenandoah。
- CPU 核数少于 4,慎用 ZGC 和 Shenandoah,并发 GC 会抢走吞吐。
-Xms与-Xmx必须相等,这条对三种 GC 都成立。- Oracle JDK 不含 Shenandoah,须换 Temurin、Corretto 等发行版。
- 选型最终靠 GC 日志的停顿数据和 P99 延迟验证,不靠感觉。
原文链接:https://www.gj0.com/thread-1811.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。