Skip to content

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

参考

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。
粤ICP备2026104257号-1