G1 和 ZGC 怎么选
一句话速记
G1 = 吞吐与延迟的均衡,适合 4G-32G 堆、TP99 在百毫秒级可接受的场景;ZGC = 超低停顿,STW 通常 < 1ms,适合大堆(32G+)或对延迟极敏感的服务。核心区别:G1 的 Mixed GC 停顿随堆大小线性增长,ZGC 通过染色指针 + 读屏障把大部分工作移到并发阶段,停顿与堆大小几乎解耦。
通俗解释(5 分钟版)
G1 的工作方式:
- 把堆切成等大的 Region(1-32MB),不再严格区分 Young/Old 物理内存
- 优先收集”回收价值最高”的 Region(Garbage First 由此得名)
- 主要停顿:Young GC(纯 STW)+ Mixed GC(收 Old Region,停顿较长)
- 每次 Mixed GC 停顿约 200-500ms(32G 堆),很难做到 10ms 以内
ZGC 的工作方式:
- 利用染色指针把 GC 状态编码进 64 位指针的高位
- 利用读屏障在用户线程访问对象时完成重映射——几乎所有工作并发进行
- STW 只有三个极短暂的阶段(初始标记 / Pause Mark Start + Relocation Pause 等),通常 < 1ms
- 代价:吞吐略低于 G1(读屏障有 CPU 开销,约 5-15%)
选型口诀:
堆 < 4G → 默认 G1 即可,也可保留 CMS(老项目)
4G-32G → G1 是主流首选
32G+ 或 TP99 < 10ms → ZGC(JDK 15+ 正式 GA,JDK 21 分代 ZGC 更好)
低 CPU 资源 / 追吞吐 → G1(ZGC 读屏障额外 CPU 开销在吞吐敏感场景吃不消)
关键细节
1)G1 的停顿来源
G1 停顿类型 停顿原因 参考时长
─────────────────────────────────────────────────────
Young GC 扫描全部 Young Region 10-200ms(取决于存活对象)
Mixed GC Young + 部分 Old Region 200-500ms(大堆更长)
Full GC(退化) G1 跟不上分配速度时触发 秒级(要避免)
G1 的痛点:Mixed GC 的 STW 时间随堆中存活对象线性增长,32G 堆停顿 1s 以上不罕见。
2)ZGC 的停顿来源(极短)
ZGC STW 阶段 典型时长
─────────────────────────────────
Pause Mark Start < 1ms(扫 GC Roots)
Pause Mark End < 1ms(最终标记)
Pause Relocate Start < 1ms(选 Region + 设读屏障)
ZGC 的停顿与堆大小无关——1G 和 1T 的停顿几乎一样短。
3)JDK 版本差异
JDK 版本 G1 状态 ZGC 状态
─────────────────────────────────────
JDK 8 默认 GC 无
JDK 11 成熟 实验性
JDK 15 成熟 正式 GA
JDK 21 成熟 分代 ZGC GA(吞吐进一步提升,推荐)
JDK 21 的分代 ZGC(-XX:+UseZGC -XX:+ZGenerational)是当前最佳选择——Young 对象单独处理,吞吐接近 G1。
4)常用 JVM 参数对比
# G1
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标最大停顿(软目标,不保证)
-XX:G1HeapRegionSize=16m # Region 大小(1-32m,2 的幂)
-XX:InitiatingHeapOccupancyPercent=45 # Old 占比多少时触发并发标记
# ZGC
-XX:+UseZGC
-XX:+ZGenerational # JDK 21+ 开启分代(推荐)
-Xms8g -Xmx8g # ZGC 强烈建议 Xms=Xmx,避免动态扩堆
-XX:ConcGCThreads=4 # 并发 GC 线程数(根据 CPU 核数调)5)生产选型决策树
线上 Java 服务需要调 GC → 先看两个指标:
│
├── 堆 ≤ 8G 且 TP99 < 200ms 可接受?
│ → G1(MaxGCPauseMillis=100-200,调好通常稳定)
│
├── 堆 8G-32G,追求低停顿?
│ → 先试 G1 + 调优,不行再迁 ZGC
│
├── 堆 > 32G,或 TP99 要求 < 10ms?
│ → ZGC(JDK 21 分代 ZGC 最佳)
│
└── 批处理 / 吞吐优先,延迟不敏感?
→ G1 或 Parallel GC(吞吐最强)
6)ZGC 读屏障的 CPU 代价
// 伪代码:ZGC 读屏障加在每次对象引用读取时
Object ref = obj.field; // 编译后插入:
// if (ref.color != expected_color) {
// ref = remap(ref); // 重映射到新地址
// }每次对象引用读取都有额外检查——CPU 密集 + 对象读取频繁的应用,ZGC 可能降吞吐 10-15%。
延伸追问
- Q:G1 的 Region 大小怎么选? → 建议让 JVM 自动算(堆大小 / 2048);大对象超过 Region 的 50% 就进 Humongous Region,容易触发 Full GC——如果有大对象频繁分配,增大 RegionSize。
- Q:ZGC 为什么能让停顿和堆大小无关? → 核心是并发转移——对象移动在并发阶段完成,用户线程通过读屏障拿到最新地址;STW 阶段只处理 GC Roots(数量固定,不随堆大小增长)。
- Q:Shenandoah 和 ZGC 有什么区别? → 两者都是超低停顿 GC,但机制不同:ZGC 用染色指针 + 读屏障;Shenandoah 用 Brooks Pointer(间接指针转发)+ 写屏障。实践中 ZGC 停顿更短,Shenandoah 在 OpenJDK 社区更活跃。
我的记法
- G1 = 均衡(堆 4-32G,百毫秒级停顿可接受)
- ZGC = 极低停顿(32G+ 或 TP99 < 10ms)
- ZGC 三关键:染色指针 + 读屏障 + 并发转移
- ZGC 代价:吞吐 -10%(读屏障 CPU 开销)
- JDK 21 分代 ZGC = 当前最佳,吞吐接近 G1
- Xms=Xmx:ZGC 必须固定堆大小
状态
- 已背速记
- 能讲通俗版
- 能答追问