← 返回文章列表

线上 Full GC 故障排查实战:从告警到根因的系统性方法论

2026年3月15日JVM调优
JVMFull GC线上排查性能优化

线上 Full GC 故障排查实战:从告警到根因的系统性方法论

在生产环境中,Full GC 告警往往意味着业务响应出现了不可容忍的停顿(STW)。面对突发的频繁 Full GC,很多时候直觉反应是“赶紧调整 JVM 参数”或者“重启机器”。但实际上,JVM 参数调优只能锦上添花,90% 的频繁 Full GC 问题,根源都出在不合理的代码逻辑或数据结构上

面对 Full GC 故障,我们需要一套克制且系统的方法论。

1. 案发现场:别急着重启,先保留证据

系统一旦重启,内存状态烟消云散,故障可能在几天后再次幽灵般重现。在处理任何 JVM 内存问题时,第一原则是:先摘除流量,然后立刻保留现场

必备的三板斧:

  • Dump 内存快照: jmap -dump:format=b,file=heap.hprof <pid>。这是分析内存泄漏的最核心文件。如果堆内存巨大(如几十GB),注意 Dump 操作本身也会引发长时间的停顿。
  • 导出线程栈: jstack <pid> > thread.log。结合 top -H -p <pid>,找出当前消耗 CPU 最多的线程,看看它们到底在干什么。
  • 分析 GC 日志: 查看发生 Full GC 时的老年代、年轻代、元空间的内存变化。重点关注:Full GC 后,老年代的内存有没有降下来?

2. 抽丝剥茧:导致 Full GC 的三大元凶

拿到现场数据后,我们需要带着假设去验证。导致 Full GC 的原因无外乎以下三种典型场景:

场景一:内存泄漏(Full GC 后老年代依然居高不下)

这是最棘手的情况。每次 Full GC 只能回收一点点内存,老年代的水位线像阶梯一样不断上涨,最终导致 OOM。

排查思路:heap.hprof 导入到 MAT(Memory Analyzer Tool)或 JProfiler 中,使用大对象视图(Dominator Tree)查看是谁占用了最多的内存。通常会发现是某些静态 HashMap 缓存忘记清理、或者 ThreadLocal 使用不当未能及时 remove() 导致的生命周期错乱。

场景二:大对象频发(内存分配速率过快)

年轻代配置合理,但依然频繁触发 Full GC。这往往是因为业务逻辑中产生了大量的“巨大对象”,导致它们无法放入 Eden 区,直接绕过年轻代晋升到了老年代(如 G1 中的 Humongous Object)。

排查思路: 常见于不良的数据库查询(例如 select * 查出了几十万条数据放到 List 里)、大文件的读取、或者是分页接口被恶意传入了 pageSize=100000。这类问题通过分析线程栈或排查慢 SQL 通常能迅速定位。

场景三:元空间(Metaspace)撑爆

在 JDK 8 之后,方法区移到了堆外的 Metaspace。如果频繁发生 Full GC,且 GC 日志显示老年代空间很充足,那极大概率是元空间扩容触发的。

排查思路: 通常与动态生成类的技术有关。例如滥用 CGLib、反射,或者在代码中频繁编译运行时的动态脚本当作新类加载。检查是否每次请求都在无限制地生成新的代理类。

3. 真实案例复盘:被忽略的流式查询

曾在线上遇到过一次 Full GC,一个系统平时很正常,国庆回来不到一周突然 Full GC告警。

公司的工具平台有 jmap 之类的能力并且把结果用火焰图呈现,查到了异常占用内存的对象,某个配置模块用 HashMap 做本地缓存,对于一条配置信息有版本控制的需求,新老版本都缓存在 HashMap 中,平时迭代多,经常重启,国庆之前封版,节后也没着急上线,导致 HashMap 中有特别多的配置信息。

恢复和修复: 集群重启,代码升级,引入 Caffeine 替换 HashMap 做本地缓存。

总结

解决线上 Full GC 问题,犹如医生看病。GC 日志和监控图表是心电图,只能告诉你病状;Heap Dump 是 X 光,能帮你找到病灶所在。而最终的药方,往往隐藏在业务代码最基础的循环和数据加载逻辑中。记住:代码质量是因,GC 只是果。

返回博客列表