Appearance
生产问题:Full GC 频繁时怎么排查
线上一旦开始频繁 Full GC,往往意味着问题已经不只是“有点慢”,而是服务稳定性开始明显变差了。
业务通常会同时看到:
- 接口抖动
- 停顿时间变长
- CPU 被 GC 抢走
- 吞吐明显下降
- 最后甚至 OOM 或服务雪崩
先说结论
Full GC 频繁时,最重要的是先回答三件事:
- GC 后堆有没有明显回落
- 哪一类对象一直留在堆里
- 是代码层对象堆积,还是 JVM 参数本身不合理
这三件事里,最先要确认的通常是第一条:
- GC 之后到底有没有“真的回收掉东西”
因为这几乎决定了后面排查是偏代码对象问题,还是偏配置与容量问题。
一个典型故障现场
比如一个 Java 服务平时 Full GC 很少,某次活动后开始出现:
- Full GC 次数持续增加
- 单次停顿从几十毫秒到几秒
- CPU 占用也同步升高
- 接口 RT 明显变差
- 但进程还没直接挂
这时如果只看到“GC 很多”,是不够的。更重要的是判断:
- GC 后堆回没回落
- 回落后为什么又很快涨回去
- 是某类对象一直活着,还是分配速率本身已经不合理
一、先看 GC 日志
先确认:
- Full GC 频率
- 单次停顿时间
- GC 后内存是否回落
同时最好再看:
- 年轻代和老年代的变化趋势
- Full GC 前后老年代占比
- 问题是突然发生,还是长期缓慢恶化
如果 GC 后堆还是一直很高,那就要高度怀疑:
- 活对象很多
- 或者存在泄漏
如果 GC 后能明显回落,但很快又被打满,则更像:
- 分配速率过高
- 突发流量或批量任务引起短时间对象暴涨
这两种方向,治理方式完全不同。
二、再看堆快照
可以抓取:
bash
jmap -dump:format=b,file=heap.hprof <pid>然后配合 MAT 去看:
- Histogram
- Dominator Tree
- Retained Heap
真正实战里,更建议先重点看:
- 哪类对象最多
- 哪类对象 retained heap 最大
- 谁持有了这些对象
- 是业务缓存、集合、ThreadLocal、结果集还是框架对象
很多 Full GC 问题,最后发现都不是神秘 JVM 黑盒,而是很朴素的对象留存。
三、先分清是“对象留存太多”还是“分配速率太猛”
1. 对象留存太多
更典型的信号是:
- GC 后堆不怎么降
- 老年代一直高
- Heap Dump 里大对象或集合明显堆积
2. 分配速率太猛
更典型的信号是:
- GC 后能回落
- 但很快又被新对象打满
- 高峰流量、批处理、导出、序列化类任务时更明显
这一步非常重要,因为很多人一看到 Full GC 就默认是“内存泄漏”,其实并不总是这样。
四、线上最常见的根因
常见并不神秘,反而很“朴素”:
- 本地缓存没有上限
- 集合无限增长
- ThreadLocal 未清理
- 大对象批量堆积
- 一次查太多数据进内存
再补几类非常高频的:
1. 查询或导出一次拉太多数据
应用本来不是常驻对象有问题,而是一次业务操作临时吃了太多堆。
2. 缓存策略没边界
最常见的是:
- 本地缓存无限增长
- key 设计没失效策略
- 批量缓存预热没有容量评估
3. ThreadLocal 和上下文对象没清
在线程池场景里尤其容易留下长期引用。
4. 消费、批量任务、报表任务叠加
平时没问题,一到高峰期或任务高峰就开始 Full GC。
5. 参数配置和实际负载不匹配
如果堆本来就偏小、代际比例不合理、容器内存边界太紧,也会更容易把问题提前放大。
五、不要一上来就只调参数
很多 Full GC 问题,最后根因不是参数,而是代码层对象模型有问题。
更稳妥的顺序是:
- 先看对象留存
- 再看分代压力
- 最后再判断是否需要调 JVM 参数
参数不是不能调,而是它通常是“在理解问题之后的收尾动作”,不是第一反应。
六、一个更实用的排查顺序
线上 Full GC 频繁时,我通常按这个顺序看:
- 先看 GC 日志,确认频率、停顿和回收后堆是否回落。
- 再判断是对象留存太多,还是分配速率太猛。
- 如果怀疑对象留存,抓 Heap Dump 看大对象和 retained heap。
- 如果更像流量或任务触发,再去看最近流量、批任务、导出、缓存预热。
- 最后才回到 JVM 参数、堆大小和容器资源边界。
这个顺序的好处是,不会把“代码对象问题”误处理成“多给点内存试试”。
一句话总结
Full GC 频繁时,真正关键的不是先问“GC 怎么调”,而是先搞清楚对象为什么回不去,或者为什么分配得这么快。
把“GC 后是否回落、对象留存、分配速率、参数边界”这几条线看清,Full GC 问题通常就能更快落到真正根因上。