众所周知,垃圾回收器(GC)对应用性能和稳定性有着决定性影响。当传统 CMS 和 Parallel GC 在大内存、低延迟场景下捉襟见肘时,G1、ZGC 和 Shenandoah 这三大现代回收器成为了工程师手中的利器。本文将带你深入其核心机制,剖析差异,并提供落地调优指南。
一、问题驱动:为什么需要新一代 GC?
想象这个场景:你负责的核心交易系统堆内存达 32GB,高峰期 CMS 的 Full GC 竟导致 15 秒服务停顿,每分钟上万的交易请求瞬间堆积。或是你的实时数据分析平台堆内存突破 100GB,Parallel GC 的 STW 让数据处理延时从 15 分钟拖到 2 小时。延迟敏感+超大堆内存,正是新一代 GC 要解决的痛点。
二、核心机制解析:从设计理念看差异
1. G1 (Garbage-First):分区回收的平衡大师
-
设计哲学:在可预测的时间内(通常 200ms)完成垃圾回收,兼顾吞吐量与延迟。
-
堆结构:将堆划分为等大小的 Region,物理隔离 Eden/Survivor/Old 区。
-
关键操作:
- 并发标记(Concurrent Marking)
- 增量复制:STW 下将多个 Region 存活对象复制到空闲 Region
- 优先回收垃圾比例高的 Region (Garbage-First)
-
痛点:存活对象集过大时,STW 时间随堆增大而上升。
示例配置(JDK 17):
-XX:+UseG1GC -Xms32g -Xmx32g # 固定堆大小 -XX:MaxGCPauseMillis=100 # 目标停顿 -XX:InitiatingHeapOccupancyPercent=40 # 老年代占比40%启动并发标记
2. ZGC (The Z Garbage Collector):革命性的并发搬运工
-
设计哲学:停顿时间恒低于 10ms,且与堆大小无关(TB 级也成立)。
-
关键技术:
- 染色指针 (Colored Pointers):在 64 位指针元数据中存储对象状态(标记/重定位)。
- 读屏障 (Load Barrier):访问对象时即时处理指针状态(无 STW)。
- 并发压缩:完全在用户线程运行时移动对象。
-
优势:停顿时间极低,堆伸缩性强。
-
代价:约 5-15% 吞吐量损失(读屏障开销),地址空间限制堆上限。
示例配置(JDK 21):
-XX:+UseZGC -Xms64g -Xmx64g -XX:+ZGenerational # 启用逻辑分代(JDK 21默认) -XX:ConcGCThreads=8 # 并发GC线程数
3. Shenandoah:并发复制的开拓者
-
设计哲学:与 ZGC 目标一致(亚毫秒停顿 + 堆大小无关),采用不同技术路径。
-
关键技术:
- Brooks 指针:每个对象头内置转发指针 (Forwarding Pointer)。
- 并发复制:在用户线程运行时复制存活对象(无需 STW)。
- 双屏障:读/写屏障联动处理对象移动。
-
优势:对非 x64 架构支持更广泛(如 ARM、RISC-V)。
-
代价:写密集型场景性能略逊于 ZGC(屏障开销更高)。
示例配置(OpenJDK 17):
-XX:+UseShenandoahGC -Xms64g -Xmx64g -XX:ShenandoahGCMode=generational # 逻辑分代 -XX:ShenandoahGarbageThreshold=20 # 内存占用20%时触发GC
三、硬核对比:选型必须考虑的 5 个维度
| 维度 | G1 | ZGC | Shenandoah |
|---|---|---|---|
| 延迟敏感性 | 中低(50-200ms) | 极高(Sub-1ms) | 极高(Sub-1ms) |
| 堆大小影响 | 停顿随堆增大而增加 | 无关(TB堆仍<1ms) | 无关(TB堆仍<1ms) |
| 压缩机制 | STW 增量复制(Young/Mixed GC) | 完全并发压缩 | 完全并发复制 |
| 分代实现 | 物理分区 (Young/Old) | 逻辑分代 (元数据标记) | 逻辑分代 (Brooks 指针标记) |
| 生产就绪版本 | JDK 7u4+ (主流 JDK 8 默认) | JDK 15+ (主流 JDK 17 可选) | JDK 15+ (OpenJDK/RedHat) |
📌 关键结论:
200ms 是你的延迟底线?选 G1 是最稳妥的方案。
需要 20ms 以下延迟 + 百 GB 级堆?ZGC/Shenandoah 是唯一选择。
四、调优实战:从日志分析到参数优化
案例:某金融系统从 G1 迁移至 ZGC 的调优过程
-
原始问题:堆 48GB 下 G1 Mixed GC 最大停顿 800ms,触发了交易超时。
-
迁移 ZGC 后现象:P99 延迟降至 5ms,但突发 P999 达 80ms。
-
诊断工具:
# 开启ZGC详细日志 -Xlog:gc*,gc+stats=info,gc+heap=info -
日志分析关键点:
[3.232s] GC(4) Pause Mark Start 0.34ms <- 正常 [3.581s] GC(4) Allocation Stall 78.4ms <- 内存分配阻塞! -
根因定位:瞬时高并发导致对象分配速率暴增,线程因等待内存挂起。
-
优化方案:
-XX:ConcGCThreads=12 # 增加并发线程数 -XX:+UseLargePages # 启用大页减少TLB Miss -XX:AllocationRateCeiling=500m # 限制最大分配速率(平滑突发流量)
五、终极选型决策树
📢 2024建议:
新项目直接采用 OpenJDK 21 + ZGC/Shenandoah(启用分代)。
存量系统若堆<32GB且延迟可接受,G1 仍是稳定选择。
六、避坑指南:最易忽略的 3 个要点
- 内存屏障开销不可轻视
ZGC/Shenandoah 的屏障 CPU 开销可达 15%,在 CPU-bound 场景需实测。 - 逻辑分代是必选项
JDK 21 的 ZGenerational 对短命对象回收效率提升 2-5 倍,务必启用! - 容器化部署的堆配置
容器内运行需显式设置-XX:+UseContainerSupport,防止 JVM 误读宿主机内存。
结语:没有银弹,只有场景
G1 的稳定、ZGC 的极致延迟、Shenandoah 的架构兼容性,印证了 GC 领域的"没有最好,只有最合适"。理解其核心机制,结合业务场景和监控数据,才能让 JVM 成为高性能系统的基石。记住:调优不是玄学,是用数据驱动的科学决策。