JVM 垃圾回收(GC):算法、分代理论与垃圾回收器全解析

130 阅读26分钟

JVM 垃圾回收的核心目标是 自动释放不再被引用的对象内存,避免内存泄漏,同时平衡 吞吐量(有效执行业务代码的时间占比)和 延迟(GC 暂停业务线程的时间)。其设计依赖三大核心:垃圾回收算法(基础逻辑)、分代收集理论(优化思路)、垃圾回收器(具体实现)。以下从这三部分层层拆解,结合机制、优缺点、适用场景展开说明。

一、基础垃圾回收算法(GC 核心逻辑)

垃圾回收算法是 GC 的底层逻辑,解决 “如何标记垃圾” 和 “如何回收垃圾” 两个核心问题。常用算法有 4 种,各有侧重:

1. 标记 - 清除算法(Mark-Sweep)

核心流程

  • 标记阶段:从 GC Roots(如栈引用、静态变量、JNI 引用)出发,遍历所有可达对象,标记为 “存活”;未被标记的则为 “垃圾”。
  • 清除阶段:遍历堆内存,回收所有未标记的垃圾对象,释放内存空间。

优缺点

  • 优点:实现简单,无需移动对象,适用于大对象较多的场景(移动大对象开销大)。

  • 缺点:

    1. 内存碎片:回收后内存空间分散,后续无法分配连续大对象(可能触发频繁 GC)。
    2. 效率较低:需两次全堆遍历(标记 + 清除),堆内存越大,耗时越长。

适用场景

  • 早期 JVM 老年代(如 CMS 回收器的初始阶段),或对象存活率高、移动成本高的场景。

2. 复制算法(Copying)

核心流程

  • 将堆内存划分为 两个大小相等的区域(From 区、To 区),同一时间仅使用其中一个(From 区)。
  • 标记阶段:标记 From 区的存活对象。
  • 复制阶段:将 From 区的存活对象复制到 To 区(按顺序排列,无碎片)。
  • 切换阶段:交换 From 区和 To 区的角色,下次 GC 针对新的 From 区执行。

优缺点

  • 优点:

    1. 无内存碎片:复制后对象连续排列,分配大对象时效率高。
    2. 效率高:仅遍历存活对象(标记 + 复制),无需处理垃圾,适合存活对象少的场景。
  • 缺点:

    1. 空间浪费:仅使用一半内存(另一半闲置),内存利用率低。
    2. 复制开销:若存活对象多(如老年代),复制成本高。

适用场景

  • 年轻代(如 Serial、Parallel Scavenge 回收器):年轻代对象 “朝生夕死”,存活比例极低,复制开销小。

3. 标记 - 整理算法(Mark-Compact)

核心流程

  • 标记阶段:与 “标记 - 清除” 一致,标记所有存活对象。
  • 整理阶段:将所有存活对象向堆内存一端移动,然后直接清理边界外的所有垃圾对象。

优缺点

  • 优点:

    1. 无内存碎片(对象连续排列)。
    2. 内存利用率 100%(无需划分双区)。
  • 缺点:

    1. 移动对象开销大:需调整所有存活对象的引用地址(如更新栈、寄存器中的指针)。
    2. 效率低于 “标记 - 清除”(多了整理步骤)。

适用场景

  • 老年代(如 Serial Old、Parallel Old 回收器):老年代对象存活率高,需避免内存碎片,且空间利用率要求高。

4. 分代收集算法(Generational Collection)

核心思想

分代收集不是独立算法,而是 基于对象生命周期特性的优化策略

  • 观察发现:JVM 中绝大多数对象 “朝生夕死”(年轻代),少数对象长期存活(老年代)。

  • 针对不同代的对象特性,选择合适的基础算法,平衡效率和空间利用率:

    1. 年轻代:用 复制算法(存活对象少,复制开销低,无碎片)。
    2. 老年代:用 标记 - 清除 或 标记 - 整理 算法(存活对象多,避免空间浪费)。

关键问题:跨代引用

  • 定义:年轻代对象引用老年代对象,或老年代对象引用年轻代对象(后者更常见)。

  • 问题:若年轻代 GC 时,需遍历老年代所有对象判断是否引用年轻代存活对象,会导致年轻代 GC 效率极低。

  • 解决方案:卡表(Card Table)

    • 将老年代内存划分为大小为 512B 的 “卡页”,用卡表记录每个卡页是否包含跨代引用。
    • 年轻代 GC 时,仅遍历卡表中 “有跨代引用” 的卡页,无需遍历整个老年代,提升标记效率。

二、分代收集理论(JVM 内存分代模型)

分代收集理论是 JVM 内存布局和 GC 策略的核心依据,其核心是 按对象存活时间划分内存区域,不同区域采用不同 GC 算法和回收器。

1. 内存分代结构(基于经典 JVM 模型,如 HotSpot)

JVM 堆内存分为 年轻代(Young Generation)老年代(Old Generation)永久代(Permanent Generation) (JDK 8 后被元空间 Metaspace 替代):

区域存储对象类型大小比例(默认)核心特性
年轻代新创建的对象、存活时间短的对象堆内存的 1/3分为 Eden 区 + 2 个 Survivor 区(From/To),比例默认 8:1:1
老年代年轻代中多次 GC 后仍存活的对象、大对象(直接进入老年代)堆内存的 2/3对象存活率高,空间大
元空间(Metaspace)存储类元信息(类名、方法、字段)、常量池、JNI 引用等(非对象堆内存)无固定大小直接使用本地内存(Native Memory),避免永久代 OOM(PermGen Space)问题

年轻代详细划分(Eden + Survivor)

  • Eden 区:所有新对象优先分配到 Eden 区(大对象除外),满了之后触发 Minor GC(年轻代 GC)。

  • Survivor 区:分为 From 和 To 区,仅一个处于 “使用中”:

    1. Minor GC 时,Eden 区和 From 区的存活对象被复制到 To 区,年龄计数器(记录 GC 次数)+1。
    2. 交换 From 和 To 区的角色(空的一方变为 To 区)。
    3. 当对象年龄达到阈值(默认 15,可通过 -XX:MaxTenuringThreshold 调整),晋升到老年代。

大对象直接进入老年代

  • 定义:超过指定大小的对象(通过 -XX:PretenureSizeThreshold 配置,默认无值,HotSpot 中默认大于 Eden 区一半的对象为大对象)。
  • 原因:避免大对象在年轻代的 Survivor 区之间频繁复制(开销大),直接分配到老年代。

2. 分代 GC 流程(Minor GC + Major GC + Full GC)

(1)Minor GC(年轻代 GC)

  • 触发条件:Eden 区满(或 Survivor 区不足)。
  • 回收范围:仅年轻代(Eden + Survivor)。
  • 算法:复制算法(高效,暂停时间短)。
  • 特点:频繁执行,暂停时间短(STW 时间短)。

(2)Major GC(老年代 GC)

  • 触发条件:老年代空间不足、永久代 / 元空间不足。
  • 回收范围:仅老年代。
  • 算法:标记 - 清除 / 标记 - 整理(效率低,暂停时间长)。
  • 特点:执行频率低,暂停时间长。

(3)Full GC(全局 GC)

  • 触发条件:Major GC 前年轻代有对象需晋升,且老年代空间不足;或调用 System.gc()(建议性,JVM 可忽略);或内存分配失败(OOM 前最后尝试)。
  • 回收范围:年轻代 + 老年代 + 永久代 / 元空间。
  • 特点:STW 时间最长,性能影响最大,应尽量避免。

三、常用垃圾回收器(HotSpot 实现)

垃圾回收器是分代理论和 GC 算法的具体实现,HotSpot 提供了多种回收器,可按 “回收方式”“分代支持”“性能目标” 分类。核心回收器的关系如下(箭头表示可搭配使用):

plaintext

年轻代回收器:Serial → Parallel Scavenge → ParNew
老年代回收器:Serial Old ← Parallel Old ← CMS
全区域回收器:G1 → ZGC → Shenandoah

1. 按核心维度分类

分类维度类型代表回收器核心目标
回收方式串行回收(单线程)Serial、Serial Old简单高效,适合单线程 / 低并发场景(如客户端应用)
并行回收(多线程)Parallel Scavenge、Parallel Old提升吞吐量(多线程同时回收,减少 GC 总时间)
并发回收(与业务线程并行)CMS、G1、ZGC、Shenandoah降低延迟(GC 线程与业务线程同时执行,减少 STW 时间)
分代支持分代回收器Serial、ParNew、Parallel 系列、CMS依赖分代模型,年轻代 / 老年代分别回收
不分代回收器G1(逻辑分代)、ZGC、Shenandoah物理上不分代(或逻辑分代),全区域统一回收,适应大内存场景
性能目标吞吐量优先Parallel Scavenge、Parallel Old最大化业务代码执行时间占比(允许较长 STW,适合后台任务)
低延迟优先CMS、G1、ZGC、Shenandoah最小化 GC 暂停时间(允许略低吞吐量,适合用户交互 / 实时系统)

2. 核心回收器详解(机制、优缺点、适用场景)

(1)串行回收器(Serial / Serial Old)

  • Serial(年轻代)

    • 机制:单线程执行 Minor GC,采用 复制算法,STW 期间仅一个 GC 线程工作,业务线程全部暂停。
    • 优点:实现简单,无线程切换开销,内存占用小。
    • 缺点:单线程效率低,不适合多核 CPU、大内存场景。
    • 适用场景:客户端应用(如桌面程序)、单核 CPU、小内存(<1GB)。
    • 开启参数:-XX:+UseSerialGC(年轻代 Serial + 老年代 Serial Old)。
  • Serial Old(老年代)

    • 机制:单线程执行 Major/Full GC,采用 标记 - 整理算法
    • 适用场景:作为 CMS 回收器的 “降级方案”(CMS 并发失败时触发),或客户端应用。

(2)并行回收器(Parallel Scavenge / Parallel Old)

  • Parallel Scavenge(年轻代)

    • 机制:多线程执行 Minor GC(线程数默认等于 CPU 核心数),采用 复制算法,吞吐量优先。
    • 核心特性:支持 “自适应调节”(-XX:+UseAdaptiveSizePolicy 开启),JVM 自动调整年轻代大小、Survivor 比例、晋升阈值等,无需手动优化。
    • 优点:吞吐量高,适合后台计算(如大数据任务),多核 CPU 下效率远高于 Serial。
    • 缺点:STW 时间随内存增大而增加,延迟不可控。
    • 适用场景:服务器应用、吞吐量优先、无低延迟要求(如批处理任务)。
    • 开启参数:-XX:+UseParallelGC(默认年轻代回收器,JDK 8+)。
  • Parallel Old(老年代)

    • 机制:多线程执行 Major/Full GC,采用 标记 - 整理算法,与 Parallel Scavenge 搭配形成 “并行全分代回收”。
    • 优点:吞吐量高,解决了 Parallel Scavenge 搭配 Serial Old 时老年代回收效率低的问题。
    • 开启参数:-XX:+UseParallelOldGC(需与 Parallel Scavenge 搭配)。

(3)并发标记清除回收器(CMS,Concurrent Mark Sweep)

  • 核心定位:老年代回收器,低延迟优先,采用 标记 - 清除算法,与 ParNew(年轻代)搭配使用。

  • 核心机制(四阶段流程,仅初始标记和重新标记会 STW)

    1. 初始标记(Initial Mark) :STW 时间短,仅标记 GC Roots 直接引用的老年代对象。
    2. 并发标记(Concurrent Mark) :无 STW,GC 线程与业务线程并行,遍历老年代所有可达对象。
    3. 重新标记(Remark) :STW 时间较短,修正并发标记期间因业务线程修改导致的 “标记失效”(如对象引用变更)。
    4. 并发清除(Concurrent Sweep) :无 STW,GC 线程与业务线程并行,回收未标记的垃圾对象。
  • 优缺点

    • 优点:低延迟(大部分阶段并发执行,STW 时间短),适合用户交互场景。

    • 缺点:

      1. 内存碎片:标记 - 清除算法导致碎片,可能触发频繁 Full GC。
      2. 并发开销:GC 线程占用 CPU 资源,降低吞吐量。
      3. 浮动垃圾:并发清除阶段产生的新垃圾(未被标记),需下次 GC 回收。
      4. 并发失败(Concurrent Mode Failure):老年代空间不足时,暂停所有线程,改用 Serial Old 回收(STW 时间长)。
  • 适用场景:低延迟要求的服务器应用(如 Web 服务),内存不太大(<16GB)。

  • 开启参数-XX:+UseConcMarkSweepGC(年轻代自动使用 ParNew)。

(4)G1 回收器(Garbage-First)

  • 核心定位:JDK 9+ 默认回收器,面向大内存(4GB+)、低延迟场景,逻辑分代、物理不分代

  • 核心机制

    1. 内存布局:将堆内存划分为多个大小相等的独立区域(Region,默认 1MB~32MB),每个 Region 可动态扮演 Eden、Survivor、老年代角色(逻辑分代)。

    2. 回收策略:优先回收 “垃圾比例最高” 的 Region(Garbage-First),平衡吞吐量和延迟。

    3. 核心流程(类似 CMS,优化 STW 阶段)

      • 初始标记(STW):标记 GC Roots 直接引用的对象。
      • 并发标记(无 STW):遍历可达对象,记录 Region 的垃圾比例。
      • 最终标记(STW):修正并发标记的偏差,使用 “SATB(Snapshot-At-The-Beginning)” 算法减少 STW 时间。
      • 筛选回收(STW):选择垃圾比例高的 Region,采用 复制算法 回收(将存活对象复制到空 Region,无碎片),可指定 STW 时间上限(-XX:MaxGCPauseMillis,默认 200ms)。
  • 优缺点

    • 优点:

      1. 低延迟:可控制 STW 时间,适合大内存场景。
      2. 无内存碎片:筛选回收阶段使用复制算法。
      3. 灵活分代:Region 动态角色切换,适应不同对象生命周期。
    • 缺点:

      1. 复杂度高:Region 管理、垃圾比例统计开销大。
      2. 小内存场景性能不如 Parallel 系列。
  • 适用场景:大内存(4GB+)、低延迟要求的服务器应用(如电商、金融系统),JDK 9+ 推荐使用。

  • 开启参数-XX:+UseG1GC(JDK 9+ 默认)。

(5)ZGC 回收器(Z Garbage Collector)

  • 核心定位:JDK 11 引入的实验性回收器,面向超大内存(16GB+)、超低延迟(STW 时间 <10ms)场景,不分代、并发回收

  • 核心机制

    1. 着色指针(Colored Pointers) :利用 64 位地址空间的高 18 位存储标记信息(如对象存活状态、Region 归属),无需额外内存存储标记位。
    2. 读屏障(Load Barrier) :当业务线程读取对象引用时,触发读屏障,根据指针颜色修正引用(如跳过已回收对象),避免 STW。
    3. Region 划分:将堆内存划分为大 Region(2MB、32MB、N×2MB),支持动态扩容 / 缩容。
    4. 并发流程:标记、回收、压缩全阶段并发执行,仅初始标记和最终标记有极短 STW(微秒级)。
  • 优缺点

    • 优点:

      1. 超低延迟:STW 时间与堆内存大小无关(即使 1TB 内存,STW 仍 <10ms)。
      2. 超大内存支持:支持 TB 级内存,适合大数据、分布式系统。
    • 缺点:

      1. 吞吐量略低:并发回收占用 CPU 资源。
      2. 依赖 64 位系统(着色指针需要 64 位地址空间)。
  • 适用场景:超大内存、超低延迟要求的场景(如实时交易、分布式缓存),JDK 17+ 已转正。

  • 开启参数-XX:+UseZGC(JDK 11+ 支持)。

(6)Shenandoah 回收器

  • 核心定位:JDK 12 引入的实验性回收器,目标与 ZGC 一致(超大内存、低延迟),不分代、并发压缩

  • 核心差异(与 ZGC)

    • 不依赖着色指针(兼容 32 位系统,或不支持着色指针的 CPU),而是通过 写屏障(Write Barrier)  实现并发标记和压缩。
    • 支持 “并发压缩”:回收时直接移动对象(无需等待空闲 Region),进一步减少延迟。
  • 适用场景:与 ZGC 重叠,适合超大内存、低延迟场景,兼容性更好(如 ARM 架构)。

  • 开启参数-XX:+UseShenandoahGC(JDK 12+ 支持,JDK 17+ 转正)。

(7)Epsilon 回收器(No-Op GC)

  • 核心定位:JDK 11 引入的 “空回收器”,不执行任何垃圾回收操作。
  • 机制:对象分配后永不回收,内存耗尽直接抛出 OOM。
  • 适用场景:性能测试(对比 GC 开销)、短期任务(如脚本执行)、已知内存不会溢出的场景。
  • 开启参数-XX:+UseEpsilonGC

3. 核心回收器对比表

回收器分代支持回收方式核心算法延迟特性吞吐量内存支持适用场景
Serial + Serial Old串行复制 / 标记 - 整理高延迟(STW 时间长)小内存(<1GB)客户端应用、单线程场景
Parallel 系列并行复制 / 标记 - 整理中延迟中内存(1GB~16GB)吞吐量优先、后台任务
CMS + ParNew并发标记 - 清除低延迟中内存(1GB~16GB)低延迟、用户交互场景
G1逻辑分代并发 + 并行筛选复制 + 标记 - 整理低延迟(可控制)中高大内存(4GB+)大内存、低延迟、通用性强
ZGC并发标记 - 复制超低延迟(<10ms)超大内存(16GB+)超大内存、超低延迟
Shenandoah并发标记 - 压缩超低延迟(<10ms)超大内存(16GB+)超大内存、低延迟、兼容性强
Epsilon无回收-无延迟(OOM 前)极高任意性能测试、短期任务

四、核心总结与选择建议

1. 核心逻辑梳理

  • 算法是基础:复制算法(年轻代)、标记 - 清除 / 整理(老年代)是核心,分代收集是优化思路。
  • 分代 vs 不分代:分代回收器(Serial、Parallel、CMS)适合中内存、传统场景;不分代回收器(G1、ZGC、Shenandoah)适合大内存、低延迟场景。
  • 性能权衡:吞吐量和延迟是矛盾的(如 Parallel 吞吐量高但延迟高,ZGC 延迟低但吞吐量略低),需根据业务场景选择。

2. 回收器选择建议

业务场景推荐回收器JDK 版本建议
客户端应用(桌面程序、小内存)Serial + Serial OldJDK 任意版本
后台任务(批处理、大数据,吞吐量优先)Parallel 系列JDK 8+
Web 服务(低延迟、中内存)G1JDK 9+(默认)
实时系统(超低延迟、超大内存)ZGC / ShenandoahJDK 17+
性能测试、短期任务EpsilonJDK 11+

3. 关键优化原则

  • 避免 Full GC:Full GC 是性能杀手,需通过调整堆大小、分代比例、晋升阈值等减少 Full GC 触发。
  • 控制年轻代大小:年轻代过大导致 Minor GC 延迟长,过小导致对象频繁晋升到老年代,触发 Major GC。
  • 大对象处理:通过 -XX:PretenureSizeThreshold 控制大对象直接进入老年代,避免年轻代复制开销。
  • 监控 GC 状态:通过 jstatjvisualvm 等工具监控 GC 次数、STW 时间,针对性优化。

JVM 垃圾回收的核心是 “因地制宜”—— 根据应用的内存规模、并发量、性能目标选择合适的回收器和参数,无需追求 “最先进”,只需 “最适合”。

五、jvm中频繁执行full gc的原因及分析

首先需区分 “真正的 Full GC”(回收年轻代 + 老年代 + 元空间)和 “老年代 GC(Major GC)”,但实际监控中两者常被统称为 “Full GC”(如 jstat 中 FGCT 统计 Full GC 次数)。常见触发条件:

  1. 老年代空间不足(最主要原因);
  2. 元空间(Metaspace)不足(JDK 8+);
  3. 年轻代晋升到老年代时,老年代剩余空间不足以容纳;
  4. 调用 System.gc()(手动触发,JVM 可忽略,但频繁调用会生效);
  5. CMS 回收器并发失败(Concurrent Mode Failure)或晋升失败(Promotion Failure);
  6. G1 回收器的 “混合回收” 触发过于频繁(本质是老年代占比过高)

一、频繁 Full GC 的 6 大核心原因(含场景 + 排查点)

1. 老年代内存分配过小,对象晋升过快

现象
  • 老年代空间占比持续超过 80%(默认阈值),Minor GC 后大量对象晋升到老年代,导致老年代快速满。
  • GC 日志中显示 Old Gen 使用率接近 100%,Full GC 后内存释放极少(因对象都是存活的)。
原因
  • 堆内存整体过小,老年代分配比例不足(默认年轻代:老年代 = 1:2,若业务中长存活对象多,需调整比例);
  • 年轻代 Survivor 区设置过小(默认 Eden:From:To=8:1:1),导致 Minor GC 时 Survivor 区无法容纳存活对象,对象直接晋升老年代(“过早晋升”);
  • 晋升阈值(-XX:MaxTenuringThreshold)设置过小(默认 15),对象未经过足够多次 Minor GC 就进入老年代,导致老年代快速积累。
示例场景
  • 业务中存在大量 “中等存活时间” 的对象(如缓存数据、会话对象),年轻代无法容纳,频繁晋升到老年代;
  • 配置参数:-Xmx2G -Xms2G -XX:NewRatio=3(年轻代 512MB,老年代 1.5GB),但业务中长存活对象峰值达 1.4GB,导致老年代频繁满。
排查点
  • 查看堆内存分配:jmap -heap <pid> 查看年轻代 / 老年代大小、比例;
  • 分析 Minor GC 晋升情况:GC 日志中 Promoted 字段(晋升到老年代的对象大小),若每次 Minor GC 晋升量接近老年代剩余空间,说明晋升过快。

2. 内存泄漏(对象无法被回收,常驻老年代)

现象
  • Full GC 后老年代内存释放极少(释放率 < 10%),且随时间推移老年代使用率持续上升,最终触发 OOM;
  • 堆 Dump 分析显示存在大量未被引用的 “无用对象”(如静态集合中的过期数据、未关闭的连接)。
常见泄漏场景(高频!)
  • 「静态集合持有对象」:static List/Map 作为缓存,只添加不清理,对象常驻老年代(如 static Map<String, User> cache = new HashMap<>(); 无过期淘汰机制);
  • 「未关闭的资源」:数据库连接、Socket 连接、文件流(InputStream/OutputStream)未在 finally 中关闭,导致连接对象及其关联资源无法回收;
  • 「ThreadLocal 滥用」:ThreadLocal 未手动 remove(),且线程池核心线程长期存活(如 Tomcat 线程池),导致 ThreadLocal 中的对象被线程持有,无法回收(形成 “线程 - ThreadLocal - 对象” 的引用链);
  • 「缓存未设置过期时间」:Redis 本地缓存、Guava Cache 未配置过期策略或淘汰机制,缓存对象持续累积;
  • 「监听器 / 回调函数未注销」:如 addListener() 后未调用 removeListener(),导致被监听对象持有大量回调实例,无法回收。
排查点
  • 生成堆 Dump:jmap -dump:format=b,file=heap.hprof <pid>,用 MAT(Memory Analyzer Tool)分析 “支配树”(Dominator Tree),定位占用内存最大的对象;
  • 检查引用链:MAT 中查看 “Path to GC Roots”,确认对象是否被静态变量、线程等 GC Roots 持有。

3. 元空间(Metaspace)不足

现象
  • GC 日志中显示 Metaspace 使用率接近 100%,触发 Full GC(元空间不足时,JVM 会触发 Full GC 尝试释放无用类元信息);
  • 频繁加载大量类,且类加载器未被回收(如动态代理生成类、热部署频繁重启、反射生成大量临时类)。
原因
  • 元空间默认无上限(依赖本地内存),但若手动设置 XX:MaxMetaspaceSize 过小(如 64MB),而业务中类加载过多,导致元空间满;
  • 类加载器泄漏:自定义类加载器(如插件化架构、热部署框架)未被回收,其加载的类元信息也无法回收(类元信息的生命周期与类加载器一致);
  • 动态生成类过多:如 Spring AOP 动态代理(CGLIB)、MyBatis 动态 SQL 生成类、Groovy 脚本编译类,且未限制生成数量。
排查点
  • 查看元空间使用情况:jstat -gcmetacapacity <pid>,关注 MCMN(最小元空间)、MCMX(最大元空间)、MC(当前元空间)、MU(已使用元空间);
  • 分析类加载情况:jmap -clstats <pid> 查看已加载的类数量、类加载器类型,若类数量持续增长,说明存在类加载泄漏。

4. GC 参数配置不合理

(1)CMS 回收器参数不当(并发失败 / 晋升失败)

CMS 是老年代并发回收器,若参数配置不合理,易导致 “并发失败”,触发 Serial Old 的 Full GC(STW 时间极长):

  • 「并发线程数不足」:-XX:ConcGCThreads(CMS 并发线程数)默认是 CPU 核心数的 1/4,若 CPU 核心少(如 2 核),并发标记 / 清除速度跟不上对象分配速度,老年代快速满,触发并发失败;
  • 「初始标记阈值过低」:-XX:CMSInitiatingOccupancyFraction(CMS 初始标记阈值,默认 92%),若老年代使用率达 92% 才触发 CMS,剩余空间不足,可能导致晋升失败(Minor GC 晋升的对象无法放入老年代),触发 Full GC;
  • 「未开启 CMS 对永久代 / 元空间的回收」:-XX:+CMSClassUnloadingEnabled 未开启,元空间不足时无法通过 CMS 回收类元信息,触发 Full GC。
(2)G1 回收器参数不当
  • 「Region 大小设置不合理」:G1 的 Region 默认 1MB~32MB,若 Region 过小,老年代 Region 数量过多,筛选回收时开销大;若过大,大对象无法放入 Region,直接晋升老年代;
  • 「MaxGCPauseMillis 设置过严」:-XX:MaxGCPauseMillis(默认 200ms)设置过小(如 50ms),G1 为满足延迟目标,会频繁触发混合回收(本质是小型 Full GC);
  • 「老年代占比阈值过高」:-XX:InitiatingHeapOccupancyPercent(IHOP,默认 45%),若老年代占比达 45% 就触发混合回收,导致频繁 Full GC。
(3)手动触发 System.gc ()
  • 业务代码中频繁调用 System.gc()(如 “优化” 内存使用,误以为调用后会释放内存),JVM 虽可忽略,但默认情况下会触发 Full GC;
  • 第三方框架 / 库调用 System.gc()(如部分老旧缓存框架),导致频繁 Full GC。
排查点
  • 查看 JVM 参数:jinfo -flags <pid>,检查 CMS/G1 相关参数、元空间大小、堆内存分配;
  • 搜索代码 / 日志:全局搜索 System.gc(),查看是否有手动调用;GC 日志中若出现 System.gc() 标记,说明是手动触发。

5. 回收器选择不当

  • 「大内存场景用 CMS」:CMS 采用标记 - 清除算法,会产生内存碎片,长期运行后碎片累积,导致无法分配大对象,触发 Full GC(碎片整理);而大内存(>16GB)应选择 G1/ZGC,其复制算法无碎片;
  • 「高并发场景用 Serial Old」:Serial Old 是单线程老年代回收器,若老年代较大(如 8GB),Full GC 时间会达秒级,且高并发下对象分配快,导致 Full GC 频繁触发;
  • 「年轻代用 Serial 回收器」:Serial 是单线程年轻代回收器,Minor GC 效率低,导致对象无法及时回收,快速晋升到老年代,间接引发频繁 Full GC。

6. 大对象直接进入老年代,占满空间

现象
  • 业务中频繁创建大对象(如超大数组、大字符串、批量数据对象),且大对象大小超过 PretenureSizeThreshold 阈值,直接进入老年代;
  • 老年代快速被大对象占满,触发 Full GC,回收后释放大量空间(因大对象可能是临时的,如批量导出数据),但随后又被新的大对象占满,形成 “频繁分配 - 回收” 循环。
原因
  • XX:PretenureSizeThreshold 配置过小(默认无值,HotSpot 中默认大于 Eden 区一半的对象为大对象),导致中等大小的对象也直接进入老年代;
  • 业务设计不合理:频繁创建超大临时对象(如每次请求创建 100MB 的数组),且未及时释放引用。
排查点
  • 查看 GC 日志:若日志中出现 Large object placed in old generation,说明有大对象直接进入老年代;
  • 分析业务代码:排查是否有批量处理、大数据导出等场景,是否存在可优化的大对象创建逻辑(如分片处理、复用对象)。

二、排查频繁 Full GC 的核心步骤(实操指南)

  1. 收集 GC 日志:添加 JVM 参数开启 GC 日志,记录关键信息:

    bash

    -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:gc.log
    

    重点关注:Full GC 触发时间、老年代 / 元空间使用情况、回收前后内存变化、STW 时间。

  2. 分析内存使用趋势

    • 用 jstat -gc <pid> 1000 实时监控堆内存变化(每 1 秒输出一次),关注 O->OU(老年代已使用)是否持续上升;
    • 用 jvisualvm 或 Grafana(结合 Prometheus)绘制老年代 / 元空间使用率曲线,判断是 “泄漏型”(持续上升)还是 “配置不当型”(波动频繁)。
  3. 生成堆 Dump 分析泄漏

    • 若老年代使用率持续上升,生成堆 Dump 用 MAT 分析,定位内存泄漏点(参考 “内存泄漏” 排查点);
    • 重点关注静态集合、ThreadLocal、未关闭的资源等高频泄漏场景。
  4. 检查 JVM 参数配置

    • 堆内存分配:-Xmx/-Xms 是否过小,-XX:NewRatio(年轻代 / 老年代比例)、-XX:SurvivorRatio(Eden/Survivor 比例)是否合理;
    • 回收器参数:CMS 的 CMSInitiatingOccupancyFractionConcGCThreads,G1 的 MaxGCPauseMillisInitiatingHeapOccupancyPercent 是否合理;
    • 元空间参数:-XX:MaxMetaspaceSize 是否过小,是否开启 CMSClassUnloadingEnabled
  5. 排查业务代码

    • 搜索 System.gc() 调用;
    • 检查大对象创建、缓存使用(是否有过期淘汰)、资源关闭(流、连接)、ThreadLocal 使用(是否 remove())。

三、优化建议(针对性解决)

  1. 调整内存分配

    • 增大堆内存:-Xmx8G -Xms8G(避免堆内存不足,根据服务器内存调整);
    • 优化分代比例:长存活对象多的场景,增大老年代比例(-XX:NewRatio=2 → 年轻代 1/3,老年代 2/3);
    • 调整 Survivor 区:增大 Survivor 区比例(-XX:SurvivorRatio=4 → Eden:From:To=4:1:1),避免过早晋升;
    • 提高晋升阈值:-XX:MaxTenuringThreshold=20(让对象多经历几次 Minor GC 再晋升)。
  2. 修复内存泄漏

    • 缓存添加过期机制:如 Guava Cache 配置 expireAfterWrite(10, TimeUnit.MINUTES),或使用 Redis 缓存替代本地静态集合;
    • 资源必须关闭:finally 中关闭流、连接(或用 try-with-resources 语法);
    • ThreadLocal 手动 remove():在请求结束或线程池任务执行完毕后,调用 threadLocal.remove()
    • 注销监听器 / 回调函数:不再使用时主动移除。
  3. 优化 GC 参数

    • CMS 优化:-XX:CMSInitiatingOccupancyFraction=75(提前触发 CMS,避免并发失败)、-XX:ConcGCThreads=4(增加并发线程数)、-XX:+CMSClassUnloadingEnabled(允许回收类元信息);
    • G1 优化:-XX:MaxGCPauseMillis=300(放宽延迟目标)、-XX:InitiatingHeapOccupancyPercent=50(提高老年代触发混合回收的阈值)、-XX:G1HeapRegionSize=8m(调整 Region 大小);
    • 禁用手动 GC:-XX:+DisableExplicitGC(忽略 System.gc() 调用)。
  4. 更换合适的回收器

    • 大内存(>4GB):改用 G1 回收器(JDK 9+ 默认),避免 CMS 的内存碎片问题;
    • 超大内存(>16GB):改用 ZGC/Shenandoah(JDK 17+ 转正),超低延迟且无碎片;
    • 吞吐量优先场景:使用 Parallel Scavenge + Parallel Old,避免 CMS 的并发开销。
  5. 优化业务代码

    • 避免频繁创建大对象:分片处理大数据、复用对象(如使用对象池);
    • 减少长存活对象:如会话数据设置合理的过期时间,避免长期驻留;
    • 动态类加载优化:避免频繁热部署、限制动态代理类的生成数量。

四、总结

频繁 Full GC 的核心原因可归纳为: “老年代 / 元空间的‘进’大于‘出’” (“进” 是对象晋升 / 类加载,“出” 是 GC 回收)。排查时需先通过 GC 日志和监控工具判断是 “配置问题” 还是 “代码问题”:

  • 若 Full GC 后内存释放较多,多是 内存分配 / 参数配置不当
  • 若 Full GC 后内存释放极少,多是 内存泄漏 / 对象存活时间过长

优化的核心思路是:减少对象进入老年代的速度 + 提高老年代对象的回收效率,最终平衡吞吐量和延迟,避免 Full GC 频繁触发。