深入理解 Java 垃圾回收器:G1、ZGC、Shenandoah 核心对比与调优实战

407 阅读5分钟

众所周知,垃圾回收器(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 个维度

维度G1ZGCShenandoah
延迟敏感性中低(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 的调优过程

  1. 原始问题​:堆 48GB 下 G1 Mixed GC 最大停顿 800ms,触发了交易超时。

  2. 迁移 ZGC 后现象​:P99 延迟降至 5ms,但突发 P999 达 80ms。

  3. 诊断工具​:

    # 开启ZGC详细日志
    -Xlog:gc*,gc+stats=info,gc+heap=info
    
  4. 日志分析关键点​:

    [3.232s] GC(4) Pause Mark Start 0.34ms  <- 正常
    [3.581s] GC(4) Allocation Stall 78.4ms <- 内存分配阻塞!
    
  5. 根因定位​:瞬时高并发导致对象分配速率暴增,线程因等待内存挂起。

  6. 优化方案​:

    -XX:ConcGCThreads=12           # 增加并发线程数
    -XX:+UseLargePages             # 启用大页减少TLB Miss
    -XX:AllocationRateCeiling=500m # 限制最大分配速率(平滑突发流量)
    

五、终极选型决策树

📢 ​2024建议​:
新项目直接采用 OpenJDK 21 + ZGC/Shenandoah(启用分代)。
存量系统若堆<32GB且延迟可接受,G1 仍是稳定选择。


六、避坑指南:最易忽略的 3 个要点

  1. 内存屏障开销不可轻视
    ZGC/Shenandoah 的屏障 CPU 开销可达 15%,​在 CPU-bound 场景需实测
  2. 逻辑分代是必选项
    JDK 21 的 ZGenerational 对短命对象回收效率提升 2-5 倍,​务必启用​!
  3. 容器化部署的堆配置
    容器内运行需显式设置 -XX:+UseContainerSupport,防止 JVM 误读宿主机内存。

结语:没有银弹,只有场景

G1 的稳定、ZGC 的极致延迟、Shenandoah 的架构兼容性,印证了 GC 领域的"没有最好,只有最合适"。理解其核心机制,结合业务场景和监控数据,才能让 JVM 成为高性能系统的基石。记住:​调优不是玄学,是用数据驱动的科学决策。​