# 7. 什么情况下会发生 Full GC?

# 标准答案

Full GC 是指 JVM 在回收堆内存时,进行的全堆扫描和回收,通常涉及 老年代(Old Generation)、永久代/元空间(Metaspace)以及可能的年轻代(Young Generation)。Full GC 可能导致 Stop-The-World(STW),影响系统性能。

Full GC 触发的常见原因包括:

  1. 老年代空间不足(如晋升失败、老年代大对象过多)。
  2. 永久代/元空间溢出(类、方法过多导致 Metaspace OOM)。
  3. GC 参数配置不当(如 System.gc()、CMS/G1 垃圾回收失败)。
  4. 大对象直接分配到老年代(超过 PretenureSizeThreshold)。
  5. SoftReference 被回收(JVM 试图回收软引用对象)。
  6. 晋升担保失败(Survivor 空间不足,无法晋升到老年代)。
  7. GC 触发策略调整(如 G1 Humongous Object 占用过多 Region)。

# 答案解析

# 1. Full GC 的工作流程

Full GC 是 JVM 最耗时 的 GC 操作,涉及的主要区域包括:

  • 老年代(Old Generation):回收长期存活的对象。
  • 永久代/元空间(PermGen/Metaspace):回收类、方法、反射数据等。
  • 年轻代(Young Generation)(可能):部分 GC 机制在 Full GC 过程中会回收年轻代。

触发 Full GC 时,所有应用线程会被暂停(STW),并使用不同 GC 算法清理对象

  • Serial/Parallel GC:采用标记-清除-整理(Mark-Sweep-Compact)。
  • CMS GC:失败时会触发 Serial Old GC(标记-清除)。
  • G1 GC:全局并发标记失败后,触发混合回收或 Full GC。

# 2. 导致 Full GC 的常见原因

# (1)老年代空间不足

JVM 的对象晋升策略决定了年轻代存活的对象会晋升到老年代。如果老年代空间不足,会触发 Full GC:

  • 大对象直接进入老年代PretenureSizeThreshold 设置过低)。
  • 长期存活对象晋升(Survivor 空间装不下,直接晋升老年代)。
  • 老年代被占满(内存泄漏、缓存过大等)。
  • GC 触发阈值-XX:InitiatingHeapOccupancyPercent 触发 G1 GC)。

示例代码

List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]); // 持续分配大对象,最终导致老年代 OOM
}
1
2
3
4

优化方案

  • 调整 -Xmx-Xms,增加老年代空间。
  • 优化对象生命周期,减少老年代内存占用。
# (2)Metaspace(元空间)溢出
  • JDK 8+ 使用 Metaspace 代替 PermGen存储类、方法、反射、动态代理等
  • 动态类过多(如大量 Spring 代理、ASM 生成的类)会导致 Metaspace OOM,引发 Full GC。

优化方案

  • 增加 Metaspace 大小
    -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
    
    1
  • 减少动态代理,优化 CGLIBASM 代码。
# (3)System.gc() 主动触发

System.gc() 可能会强制触发 Full GC,特别是在 ExplicitGCInvokesConcurrent 关闭的情况下。

优化方案

  • 禁用显式 GC 调用
    -XX:+DisableExplicitGC
    
    1
  • 使用 -XX:+ExplicitGCInvokesConcurrent(CMS/G1)避免 STW。
# (4)SoftReference 对象被回收

SoftReference(软引用)对象在内存紧张时可能触发 Full GC 来回收软引用对象。

SoftReference<byte[]> softRef = new SoftReference<>(new byte[10 * 1024 * 1024]);
System.gc(); // 可能会触发 Full GC
1
2

优化方案

  • 减少软引用使用,避免大对象 SoftReference 占用老年代。
# (5)晋升失败(Promotion Failure)
  • Survivor 空间不足,年轻代存活对象直接晋升老年代。
  • 老年代空间不足,无法容纳晋升对象,触发 Full GC。

优化方案

  • 调整 SurvivorRatio 提高 Survivor 空间:
    -XX:SurvivorRatio=8
    
    1
  • 调整 MaxTenuringThreshold 限制对象晋升:
    -XX:MaxTenuringThreshold=15
    
    1
# (6)G1 GC 回收 Humongous Object

G1 GC 的大对象(Humongous Object) 会直接进入老年代,且占用多个 Region。当 Humongous Object 过多时,可能会触发 Full GC。

优化方案

  • 避免频繁分配超大对象
    -XX:G1HeapRegionSize=16m
    
    1
  • 调整 G1 Mixed GC 触发比例
    -XX:InitiatingHeapOccupancyPercent=45
    
    1

# 3. 如何避免频繁 Full GC?

  1. 优化 GC 参数,减少老年代晋升
    -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=10
    
    1
  2. 使用 G1/ZGC,减少 Full GC 影响
    -XX:+UseG1GC -XX:G1HeapRegionSize=16m
    
    1
  3. 限制 Metaspace 使用,避免 OOM
    -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
    
    1
  4. 避免 System.gc() 触发 Full GC
    -XX:+DisableExplicitGC
    
    1
  5. 监控 GC 日志,分析 Full GC 频率
    -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
    
    1

# 4. 如何通过 GC 日志分析 Full GC?

使用 jstatGC 日志 检测 Full GC 触发情况:

jstat -gcutil <pid> 1000  # 监控 GC 状态
jmap -histo <pid>         # 查看对象分布
1
2

GC 日志示例

[GC (Allocation Failure) [Full GC (Ergonomics) 512M->300M(1024M), 1.5s]
1
  • Allocation Failure:因分配失败触发 Full GC
  • 512M->300M(1024M):GC 后从 512M 降到 300M,最大 1024M
  • 1.5s:Full GC 造成 1.5s STW,影响性能

# 深入追问

  1. 为什么 CMS 失败后会触发 Full GC?
  2. 如何优化 Metaspace 以减少 Full GC?
  3. 为什么 G1 避免了 Full GC,但仍可能触发?
  4. 如何在 生产环境 中检测 Full GC 发生的频率?

# 相关面试题

  • System.gc() 一定会触发 Full GC 吗?
  • CMS 和 G1 如何避免 Full GC?
  • 如何优化 Metaspace,防止 OOM?
  • SoftReference 在 Full GC 时会被清理吗?