频繁 Full GC 怎么排查

3 阅读7分钟

频繁 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 Failure
  • Metadata GC Threshold
  • System.gc()
  • Ergonomics
  • Promotion failed
  • Concurrent Mode Failure(CMS 常见)
  • G1 Evacuation Pause 之后跟 Full GC
  • To-space exhausted

关键判断

  • Full GC 后内存明显下降
    说明有大量可回收对象,只是回收触发太晚或对象晋升太快

  • Full GC 后内存几乎不降
    说明大概率有:

    • 内存泄漏
    • 长生命周期对象堆积
    • 缓存未释放
    • 静态集合持有对象

2. 看堆使用趋势

重点观察:

  • Eden 是否很快被打满
  • Survivor 是否放不下
  • 对象是否过早进入老年代
  • 老年代是否持续增长且回收不下来

常见情况

情况 A:年轻代太小

会导致:

  • Minor GC 很频繁
  • 存活对象很快晋升老年代
  • 老年代被迅速打满
  • 最终频繁 Full GC
情况 B:老年代太小
  • 对象正常晋升,但老年代容量不够
  • 稍微有一些长生命周期对象就触发 Full GC
情况 C:对象存活时间过长
  • 业务代码里缓存、集合、ThreadLocal、连接对象没释放
  • 导致老年代对象越来越多

3. 导出堆快照,分析到底谁占内存

这是最关键的一步。

可以在 Full GC 前后,或者内存高位时导出 heap dump。

常用工具:

  • jmap
  • MAT
  • jcmd
  • VisualVM
  • Arthas

重点看:

  • 哪个类实例数量最多
  • 哪个类占用内存最大
  • GC Roots 引用链
  • 是否存在大集合
  • 是否有缓存一直不淘汰
  • 是否有 ThreadLocal 泄漏
  • 是否有类加载器泄漏
  • 是否有重复字符串、大数组、大对象

重点分析对象

  • HashMap
  • ArrayList
  • ConcurrentHashMap
  • 缓存对象
  • 会话对象
  • 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、大查询结果、大文件处理和动态类加载等场景。