频繁Full GC排查
频繁 Full GC 是 Java 服务中比较严重的信号。它通常意味着老年代、元空间、直接内存压力或分配模式出现问题。
# 1. Full GC 的影响
Full GC 通常会带来明显 STW。
影响包括:
- 请求延迟升高。
- 吞吐下降。
- 超时增加。
- 线程堆积。
- 服务被健康检查摘除。
频繁 Full GC 不能只靠“加大堆”解决。
# 2. 先看 GC 日志
必须先确认:
- Full GC 触发原因。
- 回收前后堆变化。
- 老年代是否释放。
- 元空间是否增长。
- 是否有 System.gc。
- 是否出现 allocation failure。
- 是否是 G1 退化 Full GC。
没有 GC 日志,排查会非常低效。
# 3. 老年代持续增长
如果 Full GC 后老年代下降不明显,可能是:
- 内存泄漏。
- 长生命周期对象太多。
- 缓存过大。
- 任务堆积。
- 大量会话对象。
下一步应抓堆转储分析引用链。
# 4. 对象晋升过快
如果 Young GC 后大量对象晋升老年代,可能导致老年代快速填满。
原因包括:
- 新生代过小。
- Survivor 空间不足。
- 大量中等生命周期对象。
- 批处理一次创建大量对象。
需要结合对象分配速率和 GC 日志判断。
# 5. 大对象问题
大对象可能直接进入老年代或占用 Humongous Region。
常见来源:
- 大数组。
- 大字符串。
- 一次性读取文件。
- 大 JSON。
- 大缓存 value。
优化方向:
- 流式处理。
- 分片处理。
- 限制请求体大小。
- 避免构造巨大中间对象。
# 6. System.gc
显式调用:
System.gc();
可能触发 Full GC。
可以通过参数禁用显式 GC:
-XX:+DisableExplicitGC
但更重要的是找到调用来源,避免库或业务代码随意触发。
# 7. 元空间触发
元空间不足也可能触发 Full GC 试图卸载类。
如果类加载数量持续增长,要排查:
- 类加载器泄漏。
- 动态代理类无限生成。
- 热部署问题。
- 脚本动态编译。
# 8. 阶段小结
频繁 Full GC 的排查顺序是:看 GC 日志触发原因,看回收前后内存变化,看老年代趋势,再用堆 dump 或类加载信息定位。不要直接把 -Xmx 调大了事,那可能只是把故障推迟。
# 9. Full GC 排查决策树
频繁 Full GC
│
├─ Full GC 后老年代明显下降?
│ ├─ 是 -> 瞬时对象多 / 堆偏小 / 大对象
│ └─ 否 -> 泄漏或长生命周期对象多
│
├─ 触发原因是 Metadata GC Threshold?
│ └─ 查元空间和类加载器
│
├─ 日志有 System.gc?
│ └─ 查显式 GC 来源
│
├─ G1 出现 To-space exhausted?
│ └─ 查转移空间、分配速率、堆大小
│
└─ Humongous 对象多?
└─ 查大数组、大 JSON、大字符串
# 10. Full GC 前后怎么看
Full GC 前:Old 90%
Full GC 后:Old 88%
说明大部分对象仍然存活,泄漏或长生命周期对象嫌疑大。
Full GC 前:Old 90%
Full GC 后:Old 30%
说明能回收,但触发太频繁,可能是堆太小、瞬时峰值高、大对象或晋升过快。
# 专家速查表
| 现象 | 可能原因 | 重点证据 |
|---|---|---|
| 老年代快速上涨 | 长生命周期对象过多 | heap dump 支配树 |
| 元空间上涨 | 类加载泄漏 | 类加载器数量 |
| 晋升失败 | 年轻代对象存活过多 | GC 日志 promotion failed |
| 大对象直接进老年代 | 大数组、大缓存 | 分配栈和对象直方图 |
| System.gc 触发 | 代码或框架调用 | GC cause |
Full GC 排查不要只看次数,要结合停顿时间、触发原因、各代占用变化、对象分配速率和业务流量峰值一起判断。
# 11. Tips 快问快答
Q:Full GC 后下降很多,是不是就没问题?
A:不一定。频繁 Full GC 仍然会影响延迟,需要找为什么频繁触发。
Q:禁用 System.gc() 就能解决 Full GC 吗?
A:只能避免显式 GC 触发。如果根因是老年代压力,仍然会 Full GC。
Q:老年代持续增长一定是泄漏吗?
A:不一定。可能是正常缓存增长、流量增加或批任务;要结合业务预期和引用链。
Q:Full GC 问题优先看什么?
A:优先看 GC 日志触发原因和回收前后内存变化。