Full GC 频繁怎么排查?
更新时间:2026-09-18。问题驱动短答页。频繁 Full GC 会带来长 STW 停顿,表现为接口周期性 RT 尖刺、超时增多。
一句话回答
先用 jstat -gcutil 或 GC 日志确认 Full GC 的频率、回收前后各代占用,判断是哪类原因:回收后老年代仍降不下去多为内存泄漏/大对象,能降但很快又满多为堆太小/晋升过快,Metaspace 涨则查类加载;再用 jmap 导堆、MAT 分析大对象与引用链定位。
第一步:确认现象
bash
jstat -gcutil <pid> 1000 10
# O = 老年代使用率 FGC = Full GC 次数 FGCT = Full GC 累计耗时
# M/MU = Metaspace 使用率
# 连续看:FGC 是否在涨、每次 Full GC 后 O 能否降下来bash
# JDK8 打印 GC 详情;JDK9+ 用 -Xlog:gc*
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/gc.log按「回收后能不能降下来」分流
| 现象 | 根因方向 | 处理 |
|---|---|---|
| Full GC 后老年代仍然很高、逐次抬升 | 内存泄漏(静态集合/缓存/ThreadLocal 不清理、连接未关) | 导堆用 MAT 支配树找 GC Root 引用链 |
| 回收后能降,但很快又满 | 堆太小 / 对象晋升太快 | 扩堆、调大新生代、查过早晋升 |
| 每次都是大对象直接进老年代 | 大数组/大缓存 | 拆批、调大新生代/Region |
| Metaspace 涨触发 Full GC | 动态代理/类加载器泄漏、CGLIB 生成类过多 | -XX:MaxMetaspaceSize,查类加载器 |
| Full GC 时间点很规律 | 显式 System.gc() / RMI/NIO | -XX:+ExplicitGCInvokesConcurrent 或禁显式 GC |
| CMS 出现 Concurrent Mode Failure | 老年代碎片、回收跟不上 | 迁 G1 或调触发阈值 |
第二步:导堆定位泄漏
bash
# 在线直方图先看哪类对象最多(影响较小)
jmap -histo:live <pid> | head -30
# 导出完整堆转储(:live 会先 Full GC;生产低峰或对镜像执行)
jmap -dump:live,format=b,file=/tmp/heap.hprof <pid>
# JDK8+ 更推荐 jcmd
jcmd <pid> GC.heap_dump /tmp/heap.hprof用 MAT 打开:看 Dominator Tree(支配树) 找占用最大的对象,再看 Path to GC Roots 定位是谁引用着它不释放(常见:static Map、自建缓存无淘汰、ThreadLocal 未 remove、监听器未反注册、线程池队列积压)。
处置顺序
- 先保命:频繁长停顿影响业务时,临时扩容/滚动重启实例恢复服务。
- 留现场:重启前先
jmap/jcmd导一份堆、保留 GC 日志。 - 再定位:MAT 分析找泄漏点或调整分代/晋升参数。
- 平时加
-XX:+HeapDumpOnOutOfMemoryError,OOM 时自动留堆。
坑
- 一上来就盲目调大堆:泄漏类问题调大堆只会拉长单次 GC、推迟爆发,必须先确认回收后能不能降。
- 高峰期直接
jmap -dump:会触发 STW、生成大文件卡顿,应低峰或对流量已摘除的节点做。 - 只看 GC 频率不看停顿:G1 下 Young GC 多很正常,重点是 Full GC 与 P99 停顿。
- 忽略 Metaspace:动态代理/热部署频繁生成类也会导致 Full GC,
jstat的 M 列要一起看。
G1/CMS 行为差异见 G1 与 CMS 的区别;分代与晋升见 JVM 运行时内存与堆分代;通用内存泄漏排查见 内存泄漏排查。
总结
排查频繁 Full GC 先用 jstat -gcutil 和 GC 日志确认频率及回收前后老年代、Metaspace 占用:回收后老年代仍逐次升高是内存泄漏,用 jmap/jcmd 导堆后 MAT 支配树和 GC Root 引用链揪出静态集合、无淘汰缓存或未清理的 ThreadLocal;能降但很快又满是堆小或晋升过快,调堆和新生代;另有 Metaspace 类加载、显式 System.gc、大对象晋升、CMS 碎片等分支;处置遵循先扩容重启保命、但务必在重启前留好堆转储和 GC 日志再定位,且不要用盲目扩堆掩盖泄漏。