性能优化案例:GC 频繁排查
监控系统显示:Young GC 每秒 10-15 次,每次耗时 50-80ms。GC 总时间占总运行时间的 15%。接口延迟明显增加。
问题背景
GC 日志:
每秒 10 次 Young GC,每次 50-80ms,意味着 CPU 的 8% 在做 GC。
排查步骤
第一步:分析 GC 日志
GC 统计:
- Young GC 频率:12 次/秒
- Young GC 平均耗时:60ms
- 对象分配率:800MB/秒
- Survivor 区使用率:95%(频繁晋升)
第二步:分析分配热点
第三步:分析火焰图
问题定位到 OrderService.createOrder()。
第四步:查看代码
OrderService.java
根因分析
- 频繁创建 ArrayList/HashMap:循环中每次都 new
- 字符串拼接:
+拼接创建大量 String 对象 - 对象晋升:大对象直接进入老年代,频繁触发 Full GC
修复方案
OrderService.java
使用 ThreadLocal 复用
修复效果
排查流程总结
GC 优化策略
策略一:减少对象分配
- 复用对象(ThreadLocal、对象池)
- 使用基本类型
- 避免不必要的包装
策略二:减少大对象
策略三:调整年轻代大小
经验总结
教训一:GC 频繁要看分配率
GC 频繁的根本原因是对象分配率太高:
- GC 日志显示分配率
- async-profiler 火焰图显示分配热点
- 两者结合定位问题
教训二:对象复用要谨慎
对象复用需要考虑:
- 线程安全(ThreadLocal 是好选择)
- 对象状态清理(使用前清空)
- 内存泄漏(ThreadLocal 要及时清理)
教训三:性能测试要关注 GC
上线前压测时应该关注:
本章小结
GC 频繁排查的标准流程:
- GC 日志分析:确认分配率和 GC 频率
- 分配火焰图:定位分配热点
- 代码分析:找到高分配代码
- 对象复用:减少不必要的对象创建
- 调整参数:适当调整堆大小和 GC 参数
- 验证效果:确认 GC 改善
延伸思考
什么时候应该增加年轻代大小?
年轻代太小:
- 对象容易晋升到老年代
- Survivor 区不足以容纳存活对象
年轻代太大:
- 老年代变小,Full GC 频繁
- 单次 GC 停顿时间变长
建议:
- 观察对象晋升年龄(GC 日志中的
age) - 如果对象在年轻代经过多次 GC 才晋升,说明 Survivor 区太小
- 可以增加 Survivor 区比例:
-XX:SurvivorRatio=8