主题
Full GC 什么时候触发?五类触发条件与排查信号
问题
Full GC 的触发条件就是「老年代满了」吗?为什么看了 GC 日志,有的 Full GC 发生在老年代还远没满的时候,有的明明老年代满了却只触发了一次 Minor GC?面试面到「你们生产 Full GC 怎么排查」,如果只能答出一个「堆内存不够」,基本可以判定为没踩过真坑。
触发条件全景
Full GC 的触发路径有五条,每条对应的 GC 日志关键字不同,排查思路也不同。
1. 老年代空间不足
这是最直觉的触发条件,但具体触发场景分三种:
场景 A:Minor GC 后对象晋升失败(Handle Promotion Failure)
Minor GC 结束后,S0 或 S1 装不下存活对象,需要把对象提前晋升到老年代。如果老年代也装不下,JVM 会触发一次 Full GC。
场景 B:大对象直接进老年代
设置了 -XX:PretenureSizeThreshold,大对象绕过 Eden 直接进老年代。如果老年代此时连续空间不够(即便总空闲不少但碎片化严重),就会触发 Full GC。
场景 C:分配担保失败
-XX:HandlePromotionFailure 控制是否允许分配担保。JDK 6 Update 24 之后这个参数不再生效,JVM 的策略变成:如果老年代连续空间大于历次 Minor GC 晋升对象的平均值,就冒险 Minor GC;否则直接触发 Full GC。
日志关键行:
Full GC (Allocation Failure)注意 Allocation Failure 不一定指老年代——它在新生代分配失败时也会出现,但 Full GC 版本的 Allocation Failure 通常意味着老年代也分配失败了。
2. 元空间(Metaspace)不足
JDK 8 之后,PermGen 没了,类元数据存在 Metaspace 中。Metaspace 默认只受本地内存限制,可以通过 -XX:MaxMetaspaceSize 设上限。
当类加载频繁(热部署、动态代理、反射生成大量 Class)时,Metaspace 快速膨胀,达到上限后触发 Full GC。
日志关键行:
Full GC (Metadata GC Threshold)一个真实案例:某网关服务每 15 分钟热更新一次路由规则,用了 ASM 动态生成类,运行 3 天后 Metaspace 从 100MB 涨到 512MB,然后每 5 分钟一次 Full GC。排查时 jstat -gc_metaspace_capacity 发现 Metaspace 持续增长,dump 类对象用 java -cp 跑一个脚本统计类加载器数量,发现是热更新后旧类加载器没释放。
3. 显式调用 System.gc()
System.gc() 在源码层面只是建议 JVM 做 GC,但大多数 JVM 实现(包括 HotSpot)会直接触发 Full GC。RMI 的分布式 GC、NIO 的 DirectByteBuffer 回收、某些框架侥幸"用完调一次 GC 保平安"的写法,都是隐性源头。
日志关键行:
Full GC (System.gc())-XX:+DisableExplicitGC 可以屏蔽显式 GC,但 NIO 的 DirectByteBuffer 回收依赖 System.gc() 触发的堆外内存清理——如果你关了它,堆外内存可能泄漏,需要 -XX:+ExplicitGCInvokesConcurrent 来折中,让显式 GC 走 CMS 并发收集而不是 Full STW。
4. 统计得到的晋升悲观值(Ergonomics)
Parallel Scavenge 收集器有个「自适应调节」机制(-XX:+UseAdaptiveSizePolicy),它会根据历史 GC 统计预测下一次 Minor GC 的晋升量。如果预测值超过了老年代剩余空间,JVM 不等老年代真的满,就提前触发 Full GC。
这种 Full GC 看起来像「老年代明明还有 40% 的空闲,怎么 Full GC 了?」——其实是 JVM 的统计预测算出来的,不是真的满了。
日志关键行:
Full GC (Ergonomics)解决方式:调大 -XX:SurvivorRatio 让更多存活对象留在新生代,或调大老年代,或直接关掉自适应策略。
5. CMS 的 Concurrent Mode Failure
CMS 工作期间,业务线程还在跑,老年代空间还在被消费。如果 CMS 回收速度跟不上对象分配速度,老年代剩余空间在本轮 CMS 完成前就耗尽了,JVM 会退化为 Serial Old 做 Full GC。
日志关键行:
CMS: Concurrent Mark failed
Full GC (Concurrent Mode Failure)这是最贵的 Full GC——因为 CMS 本身已经跑了一轮标记,白做了,回退到 STW 的 Full GC 重新来一遍。典型场景:CMS 触发阈值(-XX:CMSInitiatingOccupancyFraction)设得太高(默认 92%),留给并发回收的余量太小。
排查工具箱
遇到 Full GC 频繁,按以下顺序排查:
bash
# 1. 看 GC 日志关键字
# 确定是哪类触发条件
grep "Full GC" gc.log | head -20
# 2. 看老年代使用率
# 如果 Full GC 前后老年代占用几乎不变,大概率是 Metaspace 或 System.gc()
jstat -gcutil <pid> 1000 5
# 3. 看 Metaspace 容量(如果怀疑 Metaspace)
jstat -gc <pid> | awk '{print $10, $11, $12}' # MG/MC/MN
# 4. 看显示 GC 调用
# 如果怀疑是 System.gc(),加 -XX:+PrintGCApplicationStoppedTime 看停顿
# 或直接用 -XX:+DisableExplicitGC 验证
# 5. 看晋升统计(Parallel 收集器)
-XX:+PrintAdaptiveSizePolicy调优应对
针对不同触发条件,应对方式完全不同:
| 触发条件 | 应对 |
|---|---|
| Allocation Failure | 检查大对象/晋升阈值,尝试调大老年代 |
| Metadata GC Threshold | 类加载泄漏排查,适当调大 MaxMetaspaceSize |
| System.gc() | 加 DisableExplicitGC,或改用 ExplicitGCInvokesConcurrent |
| Ergonomics | 关掉 UseAdaptiveSizePolicy,或手动调堆比例 |
| Concurrent Mode Failure | 降低 CMSInitiatingOccupancyFraction,或换 G1 |
参考
- 《深入理解 Java 虚拟机(第 3 版)》第 3 章
- JVM GC 日志解读 - Oracle 官方文档
- OpenJDK: G1 Full GC 源码分析