Java垃圾回收:如何正确选择GC收集器【演讲摘要】

Garbage Collection in Java: Choosing the Correct Collector

演讲者:Oracle Java平台组 Stefan Johansson(20年Java平台经验,12年GC领域经验)


核心主题

选择合适的GC收集器,强调升级Java版本和根据应用场景选择GC的重要性。


一、垃圾回收基础

三大评估维度:

  1. 吞吐量:单位时间完成的事务数
    • 影响因素:并发GC工作、GC暂停时间、读写屏障效率
  2. 延迟:单次事务响应时间
    • 影响因素:GC暂停、应用线程执行的GC工作、分配停顿、大对象分配
  3. 内存占用:GC算法本身的开销
    • 影响因素:GC所需 native 内存、CPU资源消耗

核心机制:

  • TLAB: 线程本地分配缓冲区,实现快速指针碰撞分配
  • 权衡本质:吞吐量与延迟不可兼得,低延迟需要并发GC,但会消耗更多CPU资源

二、OpenJDK五大收集器对比

收集器特点适用场景
Serial最低native内存开销小型容器
Parallel最佳吞吐量可接受长暂停的批处理任务
G1 (JDK9+默认)平衡吞吐量/延迟/内存占用长期运行的服务
ZGC超低延迟(<1ms),支持16TB堆低延迟要求的应用
Shenandoah并发低延迟类似ZGC场景

Oracle重点维护G1ZGC(资源集中投入)


三、G1收集器深度解析

核心目标: 平衡性能,避免最坏情况延迟,通过混合收集分散Full GC压力

演进历程(JDK8→25):

  • JDK9: 成为默认GC(当时性能尚不成熟)
  • JDK11: 降低native内存使用(1.3GB→更低),引入并行Full GC
  • JDK17: 降低暂停时间,继续优化内存
  • JDK21: 重大改进 - 移除第二个标记位图,native内存降至440MB(16GB堆场景)
  • JDK25: 可扩展性改进,屏障优化(未正式发布,即将推出),吞吐量接近Parallel

性能数据(16GB堆):

  • 暂停时间:JDK25平均70ms,最坏情况显著改善
  • 内存占用:JDK8需1.3GB额外内存 → JDK25仅需440MB

调优参数:

  • 唯一核心参数:-XX:MaxGCPauseMillis(默认200ms)
    • 调低(如50ms):提升延迟,增加GC频率
    • 调高:提升吞吐量

四、ZGC收集器深度解析

核心目标: 始终低于1ms暂停,支持超大堆(16TB)

演进历程:

  • JDK11: 实验性特性(Linux only),目标<10ms
  • JDK15: 生产就绪,支持多平台
  • JDK17: 达成亚毫秒级暂停(~200μs),极简化配置
  • JDK21: 引入分代模式-XX:+ZGenerational),显著提升吞吐量
  • JDK25: 分代模式成为唯一模式,优化大堆场景

关键特性:

  • 分代模式:大幅提升分配速率承受能力,避免分配停顿
  • 易用性:几乎零配置(仅需设置堆大小),自动调优
  • 适用性不仅限于大堆,1GB堆起步,取决于分配速率

性能数据:

  • 暂停时间:分代模式下**~100μs**(0.1ms)
  • 吞吐量:分代模式比非分代提升显著

五、GC调优实践指南

1. 基本原则

  • 避免过度调优:现代GC(G1/ZGC)已高度自动化
  • 重新审视旧参数:旧参数可能在新GC上产生负面效果
    • 示例:手动设置年轻代大小会干扰G1的自适应策略
  • 对比测试:至少测试G1和ZGC,全面评估吞吐量/延迟/资源消耗

2. 堆大小设置(XXI)

  • 默认:最大堆为系统内存的25%
  • 关键原则
    • G1:避免Full GC(增大堆)
    • ZGC:避免分配停顿(增大堆)
  • 未来方向自动堆大小调整(开发中),根据系统负载动态调整

3. 低层优化(“免费午餐”)

  • 大页(Huge Pages):Linux上可提升~10%性能
    • 显式大页(HugeTLB):需预配置,行为确定
    • 透明大页(THP):动态但可能碎片化,新版内核表现良好
    • 注意:G1使用匿名内存(默认always),ZGC使用共享内存(默认never),需统一配置
  • 紧凑对象头(JDK25+ -XX:+CompactObjectHeaders):
    • 对象头从12/16字节 → 8字节
    • 显著减少堆占用 → 更少GC → 更高吞吐量
  • NUMA支持:ZGC默认开启,G1需手动启用(-XX:+UseNUMA
  • 预触摸内存-XX:+AlwaysPreTouch(配合固定堆),避免首次访问延迟

六、监控与验证

JDK内置工具:

  1. JVM日志-Xlog:gc*(详细GC日志)
  2. Flight Recorder (JFR) + JDK Mission Control
    • 查看暂停时间统计、分配停顿、CPU/内存占用
    • 关键视图:jfr view gc-pauses, jfr view native-memory

核心指标:

  • 延迟:应用级响应时间(首选)或GC暂停时间统计
  • ZGC专项:必须检查分配停顿(Allocation Stall)
  • 资源:OS级监控 + JVM native内存跟踪(-XX:NativeMemoryTracking=summary

七、案例研究:SPECjbb2015基准测试

测试配置:

  • 固定吞吐量(比ZGC最大吞吐低30%)
  • 16GB堆
  • 对比G1 vs ZGC(分代模式)

结果对比:

指标G1ZGC(分代)结论
延迟(P99.9)~100ms40-50msZGC显著更优
平均GC暂停65ms0.1msZGC亚毫秒级
CPU占用平均50%,峰值90%平均67%,峰值94%ZGC消耗更多CPU
Native内存峰值440MB (~2.5%)峰值~1GB (~6%)ZGC内存开销更高

关键发现:

  • ZGC的40-50ms响应时间异常由应用线程辅助GC工作导致(非GC暂停)
  • G1的暂停时间始终低于200ms默认目标
  • 不存在绝对最优解:取决于业务对延迟/资源/吞吐的优先级

八、案例研究:Cassandra数据库

测试目标: 对比不同客户端负载下的响应时间

结果:

  • 吞吐量视角(平均响应时间):G1略优(非延迟指标)
  • 延迟视角(P99响应时间):
    • 非分代ZGC:75客户端后分配停顿激增 → 延迟暴涨
    • 分代ZGC:支持275+客户端,保持超低延迟
    • G1(默认200ms):最坏情况<200ms
    • G1(调优50ms):延迟显著改善,接近ZGC水平

结论:分代模式对ZGC至关重要,G1通过简单调优也能获得优秀延迟


九、核心建议

  1. 升级JDK版本:新版本GC性能大幅提升(尤其是JDK21+)
  2. 按需选择GC
    • 默认/通用:G1(平衡,低资源)
    • 超低延迟:ZGC(分代模式,资源充足场景)
  3. 谨慎调优:先测试默认行为,再针对性调整
  4. 全面评估:不仅看延迟,还要权衡吞吐量和资源占用
  5. 持续学习:关注GC调优指南、Inside.java、Java YouTube频道

十、未来方向

  • 自动堆大小调整:G1和ZGC均在开发中
  • ZGC线程本地GC:探索性工作
  • G1吞吐量优化:屏障改进即将发布

总结:现代Java GC已高度成熟,升级版本正确选型比复杂调优更重要。G1是平衡之选,ZGC是延迟之王,两者都能通过极简配置满足绝大多数场景。