Java垃圾回收:如何正确选择GC收集器【演讲摘要】
Garbage Collection in Java: Choosing the Correct Collector
演讲者:Oracle Java平台组 Stefan Johansson(20年Java平台经验,12年GC领域经验)
核心主题
选择合适的GC收集器,强调升级Java版本和根据应用场景选择GC的重要性。
一、垃圾回收基础
三大评估维度:
- 吞吐量:单位时间完成的事务数
- 影响因素:并发GC工作、GC暂停时间、读写屏障效率
- 延迟:单次事务响应时间
- 影响因素:GC暂停、应用线程执行的GC工作、分配停顿、大对象分配
- 内存占用:GC算法本身的开销
- 影响因素:GC所需 native 内存、CPU资源消耗
核心机制:
- TLAB: 线程本地分配缓冲区,实现快速指针碰撞分配
- 权衡本质:吞吐量与延迟不可兼得,低延迟需要并发GC,但会消耗更多CPU资源
二、OpenJDK五大收集器对比
| 收集器 | 特点 | 适用场景 |
|---|---|---|
| Serial | 最低native内存开销 | 小型容器 |
| Parallel | 最佳吞吐量 | 可接受长暂停的批处理任务 |
| G1 (JDK9+默认) | 平衡吞吐量/延迟/内存占用 | 长期运行的服务 |
| ZGC | 超低延迟(<1ms),支持16TB堆 | 低延迟要求的应用 |
| Shenandoah | 并发低延迟 | 类似ZGC场景 |
Oracle重点维护:G1 和 ZGC(资源集中投入)
三、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内置工具:
- JVM日志:
-Xlog:gc*(详细GC日志) - 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(分代模式)
结果对比:
| 指标 | G1 | ZGC(分代) | 结论 |
|---|---|---|---|
| 延迟(P99.9) | ~100ms | 40-50ms | ZGC显著更优 |
| 平均GC暂停 | 65ms | 0.1ms | ZGC亚毫秒级 |
| 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通过简单调优也能获得优秀延迟
九、核心建议
- 升级JDK版本:新版本GC性能大幅提升(尤其是JDK21+)
- 按需选择GC:
- 默认/通用:G1(平衡,低资源)
- 超低延迟:ZGC(分代模式,资源充足场景)
- 谨慎调优:先测试默认行为,再针对性调整
- 全面评估:不仅看延迟,还要权衡吞吐量和资源占用
- 持续学习:关注GC调优指南、Inside.java、Java YouTube频道
十、未来方向
- 自动堆大小调整:G1和ZGC均在开发中
- ZGC线程本地GC:探索性工作
- G1吞吐量优化:屏障改进即将发布
总结:现代Java GC已高度成熟,升级版本和正确选型比复杂调优更重要。G1是平衡之选,ZGC是延迟之王,两者都能通过极简配置满足绝大多数场景。