GC 调优案例
2019年双十二,我带着团队排查了一个持续了三天的 GC 问题。
现象很奇怪:白天服务正常,晚上高峰期就开始 Full GC,每次持续 5~15 秒。重启能管半小时,然后又复发。
排查了三天,最后发现罪魁祸首是一个用 HashMap 做缓存的接口——每天晚上 8 点会有大批用户同时访问,导致 HashMap 持续膨胀,撑爆了老年代。
这个案例教会我一件事:GC 调优的本质不是调参数,是找代码里的内存漏洞。
一、GC 调优的核心方法论 🔴
1.1 调优前必读:GC 的不可能三角
- 吞吐量优先(Parallel GC):追求高吞吐量,停顿时间长
- 延迟优先(G1/ZGC/Shenandoah):追求低停顿,吞吐量略低
- 内存效率:堆越大,GC 频率越低,但单次 GC 时间越长
没有最优解,只有 tradeoff。 调优的第一步是明确你的目标:是对延迟敏感(延迟敏感型),还是对吞吐量敏感(吞吐量优先型)?
1.2 GC 调优的标准流程
二、案例一:CMS Old 区持续增长 🟡
2.1 问题背景
生产环境:
- 服务:用户画像计算服务
- 堆大小:8GB(Old 区约 5.5GB)
- GC 策略:CMS + ParNew
- 问题:Old 区使用量从 3GB 持续增长到 5GB,每 2 小时触发一次 Full GC
2.2 根因分析
通过 MAT 分析堆转储,发现两个问题:
问题一:缓存对象没有过期机制
这个缓存没有大小限制、没有过期机制,随着用户量增长无限膨胀。
问题二:大对象直接进入老年代
orders 列表的大小超过了 PretenureSizeThreshold,直接进入 Old 区。
2.3 调优方案
方案 A:接入 Redis 缓存(推荐)
将本地缓存替换为 Redis 缓存,彻底解决内存占用问题。但需要评估 Redis 的 QPS 承受能力。
方案 B:限制本地缓存大小
调优后效果:
2.4 调优参数参考
面试加分点:能说出"CMS 的 CMSInitiatingOccupancyFraction 不是设得越低越好——设得太低会提前触发并发回收,增加 CMS 线程的 CPU 开销,反而影响吞吐量。推荐设置为 68%~75% 之间",说明他理解了这个参数的工程代价。
三、案例二:G1 Mixed GC 频繁触发 🟡
3.1 问题背景
生产环境:
- 服务:商品推荐引擎
- 堆大小:16GB
- GC 策略:G1
- 问题:Mixed GC 频繁触发,每次停顿时间超过 300ms,P99 延迟不达标
3.2 根因分析
G1 的 Mixed GC 会同时回收年轻代和老年代的 Region。如果老年代的垃圾比例不高(大量是存活对象),G1 仍然会回收这些 Region,浪费 GC 时间。
问题本质:老年代中存在大量长期存活但仍被回收的 Region——这些 Region 里的对象大部分是"准垃圾"(接近死亡的对象)。
3.3 调优方案
调优后效果:
四、案例三:ZGC 大内存低延迟 🟢
4.1 问题背景
生产环境:
- 服务:实时数据分析平台
- 堆大小:64GB
- GC 策略:初始为 G1,调优后切 ZGC
- 问题:即使调了 G1 参数,P99 GC 停顿仍然超过 200ms
4.2 调优方案
调优后效果:
ZGC 不是银弹。ZGC 的并发阶段会消耗更多 CPU(大约增加 5%~10% 的 CPU 开销)。在高 CPU 密集型应用中,切换到 ZGC 可能导致 CPU 打满。在切换前务必做容量评估。
五、GC 调优参数速查表 🟢
5.1 通用调优参数
5.2 G1 专项参数
5.3 CMS 专项参数
【架构权衡】 GC 调优的参数有几十个,但调参只解决 30% 的问题,代码优化解决 70%。真正的高手不会沉迷于调参,而是会:
- 通过 GC 日志定位到问题代码
- 分析对象的生命周期设计
- 用更合适的数据结构或缓存策略从根本上减少 GC 压力
参数调优是应急手段,代码优化才是根治之道。