频繁 Full GC,一般从这几个方向答:
现象确认 → 看 GC 日志 → 判断是内存不够、对象晋升过快,还是代码/配置问题 → 再定位具体对象和原因。
一、先说结论
频繁 Full GC 的排查思路,本质上就是先确认 Full GC 触发原因,再结合 GC 日志、堆内存快照、对象分配情况和 JVM 参数,判断到底是老年代空间不足、元空间不足、大对象/长生命周期对象过多,还是显式调用 System.gc()、参数配置不合理导致的。
二、先搞清楚什么叫 Full GC
Full GC 通常指:
- 回收 整个堆,尤其是老年代
- 很多时候还会连带方法区/元空间一起看
- 一般会伴随较长时间 STW
所以一旦频繁 Full GC,通常说明:
- 老年代压力很大
- 内存回收效率差
- 对象生命周期异常
- 或 JVM 配置有问题
三、排查步骤
1. 先看 GC 日志,确认 Full GC 为什么发生
第一步不是猜,而是看日志。
重点看:
- Full GC 触发前后堆使用量
- 年轻代回收是否正常
- 老年代是否持续上涨
- Full GC 后老年代是否明显下降
- Full GC 的触发原因
常见触发原因有:
Allocation FailureMetadata GC ThresholdSystem.gc()ErgonomicsPromotion failedConcurrent Mode Failure(CMS 常见)G1 Evacuation Pause之后跟 Full GCTo-space exhausted
关键判断
-
Full GC 后内存明显下降
说明有大量可回收对象,只是回收触发太晚或对象晋升太快 -
Full GC 后内存几乎不降
说明大概率有:- 内存泄漏
- 长生命周期对象堆积
- 缓存未释放
- 静态集合持有对象
2. 看堆使用趋势
重点观察:
- Eden 是否很快被打满
- Survivor 是否放不下
- 对象是否过早进入老年代
- 老年代是否持续增长且回收不下来
常见情况
情况 A:年轻代太小
会导致:
- Minor GC 很频繁
- 存活对象很快晋升老年代
- 老年代被迅速打满
- 最终频繁 Full GC
情况 B:老年代太小
- 对象正常晋升,但老年代容量不够
- 稍微有一些长生命周期对象就触发 Full GC
情况 C:对象存活时间过长
- 业务代码里缓存、集合、ThreadLocal、连接对象没释放
- 导致老年代对象越来越多
3. 导出堆快照,分析到底谁占内存
这是最关键的一步。
可以在 Full GC 前后,或者内存高位时导出 heap dump。
常用工具:
jmapMATjcmd- VisualVM
- Arthas
重点看:
- 哪个类实例数量最多
- 哪个类占用内存最大
- GC Roots 引用链
- 是否存在大集合
- 是否有缓存一直不淘汰
- 是否有 ThreadLocal 泄漏
- 是否有类加载器泄漏
- 是否有重复字符串、大数组、大对象
重点分析对象
HashMapArrayListConcurrentHashMap- 缓存对象
- 会话对象
- MQ 消息堆积对象
- 数据库查询结果集
- 静态变量引用对象
ThreadLocalMap
4. 判断是不是内存泄漏
频繁 Full GC 最怕的是泄漏。
泄漏的典型特征
- 老年代使用量随着时间持续增长
- Full GC 后只下降一点,甚至几乎不降
- 最终 OOM
常见泄漏点
- 静态集合持有对象
- 缓存没有过期策略
- ThreadLocal 使用后没 remove
- 连接、流、会话对象没关闭
- 监听器/回调未注销
- 第三方框架持有引用
- 本地内存泄漏(如 DirectByteBuffer)
5. 看是否是大对象或大数组问题
有些系统不是泄漏,而是瞬时大对象很多。
比如:
- 一次查出大量数据
- 大 JSON 反序列化
- 大文件读取到内存
- 大批量导出
- 大图片处理
- 大 byte[] 缓冲区
这些会导致:
- 年轻代放不下,直接进入老年代
- 老年代迅速被打满
- 引发 Full GC
6. 排查是否显式调用了 System.gc()
有些 Full GC 不是 JVM 自己想做,而是代码主动触发的。
排查:
- 全局搜索
System.gc() - 看第三方框架是否调用
- 看监控平台、RMI、某些中间件是否触发
如果日志里有明显 System.gc() 痕迹,就要重点看这里。
很多线上系统会通过参数抑制:
-XX:+DisableExplicitGC
7. 看元空间/方法区是否有问题
有时候频繁 Full GC 不是堆不够,而是 Metaspace 压力大。
常见原因:
- 动态生成类过多
- 频繁热部署
- CGLIB / ByteBuddy 动态代理大量生成
- 类加载器泄漏
- JSP/脚本引擎反复编译类
特征:
- GC 日志里出现
Metadata GC Threshold Metaspace持续增长
8. 看收集器和参数是否合理
有些 Full GC 不是代码本身有问题,而是 JVM 配置不合适。
重点看:
- 堆大小是否合理:
-Xms -Xmx - 新生代是否过小:
-Xmn - Survivor 比例是否不合理
- 晋升阈值是否过低:
-XX:MaxTenuringThreshold - CMS / G1 参数是否合理
- 是否选错垃圾收集器
举例
CMS
如果出现:
Concurrent Mode Failure
说明 CMS 并发回收还没完成,老年代就满了,退化成 Full GC
G1
如果出现:
to-space exhausted- evacuation 失败
说明 Region 回收/复制空间不足,也可能退化成 Full GC
四、常见原因总结
1. 内存泄漏
- 静态变量、集合、ThreadLocal、缓存未释放
2. 老年代空间不足
- 堆本来就配小了
- 长生命周期对象较多
3. 新生代太小
- 对象过早晋升老年代
4. 大对象频繁创建
- 大数组、大 JSON、大查询结果
5. 元空间不足
- 动态类加载过多
- 类加载器泄漏
6. 显式 GC
System.gc()或第三方库触发
7. GC 参数不合理
- 收集器选择或参数配置不匹配业务场景
五、实际排查时常用命令
查看 GC 情况
jstat -gcutil <pid> 1000
查看堆信息
jmap -heap <pid>
导出堆快照
jmap -dump:live,format=b,file=heap.hprof <pid>
查看类实例分布
jmap -histo:live <pid>
JDK 9+ 更推荐
jcmd <pid> GC.heap_info
jcmd <pid> GC.class_histogram
jcmd <pid> GC.heap_dump heap.hprof
六、面试推荐回答
频繁 Full GC 我会先看 GC 日志,确认 Full GC 的触发原因和回收前后的内存变化。如果 Full GC 后老年代占用仍然很高,我会优先怀疑内存泄漏或长生命周期对象堆积,然后通过 jmap、jcmd 导出堆快照,用 MAT 分析大对象、对象数量和 GC Roots 引用链。如果 Full GC 后内存能明显下降,我会进一步看是否是新生代过小、对象晋升过快、大对象直接进入老年代,或者 JVM 参数配置不合理。另外还会排查是否存在 System.gc()、元空间不足、动态代理类过多、类加载器泄漏等问题。最终结合业务代码中的缓存、集合、ThreadLocal、批量查询和大文件处理等场景定位根因。
七、面试官最喜欢追问的几个点
1. Full GC 后内存降不下来说明什么?
说明大概率:
- 内存泄漏
- 长生命周期对象被引用着
- 缓存/集合没有释放
2. Full GC 后内存能降很多,为什么还频繁?
说明不是泄漏,更可能是:
- 对象创建太快
- 新生代太小
- 晋升太快
- 大对象太多
- 参数不合理
3. 哪些代码最容易引发频繁 Full GC?
- 大分页一次性查太多数据
- 缓存无上限
- ThreadLocal 不清理
- 大量字符串拼接
- 大文件一次性读入
- 大对象反复创建
- 动态代理类无限增长
八、背诵版
频繁 Full GC 的排查思路是先通过 GC 日志确认触发原因,重点看 Full GC 前后的堆内存变化以及老年代回收效果。如果 Full GC 后内存几乎不下降,通常说明存在内存泄漏或长生命周期对象堆积,需要通过 jmap、jcmd 导出堆快照,再用 MAT 分析大对象、对象数量和 GC Roots 引用链。如果 Full GC 后内存能明显下降,则更多考虑新生代过小、对象晋升过快、大对象直接进入老年代、显式调用 System.gc()、元空间不足或 JVM 参数配置不合理等问题。排查时重点关注缓存、静态集合、ThreadLocal、大查询结果、大文件处理和动态类加载等场景。