JVM 垃圾回收的核心目标是 自动释放不再被引用的对象内存,避免内存泄漏,同时平衡 吞吐量(有效执行业务代码的时间占比)和 延迟(GC 暂停业务线程的时间)。其设计依赖三大核心:垃圾回收算法(基础逻辑)、分代收集理论(优化思路)、垃圾回收器(具体实现)。以下从这三部分层层拆解,结合机制、优缺点、适用场景展开说明。
一、基础垃圾回收算法(GC 核心逻辑)
垃圾回收算法是 GC 的底层逻辑,解决 “如何标记垃圾” 和 “如何回收垃圾” 两个核心问题。常用算法有 4 种,各有侧重:
1. 标记 - 清除算法(Mark-Sweep)
核心流程
- 标记阶段:从 GC Roots(如栈引用、静态变量、JNI 引用)出发,遍历所有可达对象,标记为 “存活”;未被标记的则为 “垃圾”。
- 清除阶段:遍历堆内存,回收所有未标记的垃圾对象,释放内存空间。
优缺点
-
优点:实现简单,无需移动对象,适用于大对象较多的场景(移动大对象开销大)。
-
缺点:
- 内存碎片:回收后内存空间分散,后续无法分配连续大对象(可能触发频繁 GC)。
- 效率较低:需两次全堆遍历(标记 + 清除),堆内存越大,耗时越长。
适用场景
- 早期 JVM 老年代(如 CMS 回收器的初始阶段),或对象存活率高、移动成本高的场景。
2. 复制算法(Copying)
核心流程
- 将堆内存划分为 两个大小相等的区域(From 区、To 区),同一时间仅使用其中一个(From 区)。
- 标记阶段:标记 From 区的存活对象。
- 复制阶段:将 From 区的存活对象复制到 To 区(按顺序排列,无碎片)。
- 切换阶段:交换 From 区和 To 区的角色,下次 GC 针对新的 From 区执行。
优缺点
-
优点:
- 无内存碎片:复制后对象连续排列,分配大对象时效率高。
- 效率高:仅遍历存活对象(标记 + 复制),无需处理垃圾,适合存活对象少的场景。
-
缺点:
- 空间浪费:仅使用一半内存(另一半闲置),内存利用率低。
- 复制开销:若存活对象多(如老年代),复制成本高。
适用场景
- 年轻代(如 Serial、Parallel Scavenge 回收器):年轻代对象 “朝生夕死”,存活比例极低,复制开销小。
3. 标记 - 整理算法(Mark-Compact)
核心流程
- 标记阶段:与 “标记 - 清除” 一致,标记所有存活对象。
- 整理阶段:将所有存活对象向堆内存一端移动,然后直接清理边界外的所有垃圾对象。
优缺点
-
优点:
- 无内存碎片(对象连续排列)。
- 内存利用率 100%(无需划分双区)。
-
缺点:
- 移动对象开销大:需调整所有存活对象的引用地址(如更新栈、寄存器中的指针)。
- 效率低于 “标记 - 清除”(多了整理步骤)。
适用场景
- 老年代(如 Serial Old、Parallel Old 回收器):老年代对象存活率高,需避免内存碎片,且空间利用率要求高。
4. 分代收集算法(Generational Collection)
核心思想
分代收集不是独立算法,而是 基于对象生命周期特性的优化策略:
-
观察发现:JVM 中绝大多数对象 “朝生夕死”(年轻代),少数对象长期存活(老年代)。
-
针对不同代的对象特性,选择合适的基础算法,平衡效率和空间利用率:
- 年轻代:用 复制算法(存活对象少,复制开销低,无碎片)。
- 老年代:用 标记 - 清除 或 标记 - 整理 算法(存活对象多,避免空间浪费)。
关键问题:跨代引用
-
定义:年轻代对象引用老年代对象,或老年代对象引用年轻代对象(后者更常见)。
-
问题:若年轻代 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 区,仅一个处于 “使用中”:
- Minor GC 时,Eden 区和 From 区的存活对象被复制到 To 区,年龄计数器(记录 GC 次数)+1。
- 交换 From 和 To 区的角色(空的一方变为 To 区)。
- 当对象年龄达到阈值(默认 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) :
- 初始标记(Initial Mark) :STW 时间短,仅标记 GC Roots 直接引用的老年代对象。
- 并发标记(Concurrent Mark) :无 STW,GC 线程与业务线程并行,遍历老年代所有可达对象。
- 重新标记(Remark) :STW 时间较短,修正并发标记期间因业务线程修改导致的 “标记失效”(如对象引用变更)。
- 并发清除(Concurrent Sweep) :无 STW,GC 线程与业务线程并行,回收未标记的垃圾对象。
-
优缺点:
-
优点:低延迟(大部分阶段并发执行,STW 时间短),适合用户交互场景。
-
缺点:
- 内存碎片:标记 - 清除算法导致碎片,可能触发频繁 Full GC。
- 并发开销:GC 线程占用 CPU 资源,降低吞吐量。
- 浮动垃圾:并发清除阶段产生的新垃圾(未被标记),需下次 GC 回收。
- 并发失败(Concurrent Mode Failure):老年代空间不足时,暂停所有线程,改用 Serial Old 回收(STW 时间长)。
-
-
适用场景:低延迟要求的服务器应用(如 Web 服务),内存不太大(<16GB)。
-
开启参数:
-XX:+UseConcMarkSweepGC(年轻代自动使用 ParNew)。
(4)G1 回收器(Garbage-First)
-
核心定位:JDK 9+ 默认回收器,面向大内存(4GB+)、低延迟场景,逻辑分代、物理不分代。
-
核心机制:
-
内存布局:将堆内存划分为多个大小相等的独立区域(Region,默认 1MB~32MB),每个 Region 可动态扮演 Eden、Survivor、老年代角色(逻辑分代)。
-
回收策略:优先回收 “垃圾比例最高” 的 Region(Garbage-First),平衡吞吐量和延迟。
-
核心流程(类似 CMS,优化 STW 阶段) :
- 初始标记(STW):标记 GC Roots 直接引用的对象。
- 并发标记(无 STW):遍历可达对象,记录 Region 的垃圾比例。
- 最终标记(STW):修正并发标记的偏差,使用 “SATB(Snapshot-At-The-Beginning)” 算法减少 STW 时间。
- 筛选回收(STW):选择垃圾比例高的 Region,采用 复制算法 回收(将存活对象复制到空 Region,无碎片),可指定 STW 时间上限(
-XX:MaxGCPauseMillis,默认 200ms)。
-
-
优缺点:
-
优点:
- 低延迟:可控制 STW 时间,适合大内存场景。
- 无内存碎片:筛选回收阶段使用复制算法。
- 灵活分代:Region 动态角色切换,适应不同对象生命周期。
-
缺点:
- 复杂度高:Region 管理、垃圾比例统计开销大。
- 小内存场景性能不如 Parallel 系列。
-
-
适用场景:大内存(4GB+)、低延迟要求的服务器应用(如电商、金融系统),JDK 9+ 推荐使用。
-
开启参数:
-XX:+UseG1GC(JDK 9+ 默认)。
(5)ZGC 回收器(Z Garbage Collector)
-
核心定位:JDK 11 引入的实验性回收器,面向超大内存(16GB+)、超低延迟(STW 时间 <10ms)场景,不分代、并发回收。
-
核心机制:
- 着色指针(Colored Pointers) :利用 64 位地址空间的高 18 位存储标记信息(如对象存活状态、Region 归属),无需额外内存存储标记位。
- 读屏障(Load Barrier) :当业务线程读取对象引用时,触发读屏障,根据指针颜色修正引用(如跳过已回收对象),避免 STW。
- Region 划分:将堆内存划分为大 Region(2MB、32MB、N×2MB),支持动态扩容 / 缩容。
- 并发流程:标记、回收、压缩全阶段并发执行,仅初始标记和最终标记有极短 STW(微秒级)。
-
优缺点:
-
优点:
- 超低延迟:STW 时间与堆内存大小无关(即使 1TB 内存,STW 仍 <10ms)。
- 超大内存支持:支持 TB 级内存,适合大数据、分布式系统。
-
缺点:
- 吞吐量略低:并发回收占用 CPU 资源。
- 依赖 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 Old | JDK 任意版本 |
| 后台任务(批处理、大数据,吞吐量优先) | Parallel 系列 | JDK 8+ |
| Web 服务(低延迟、中内存) | G1 | JDK 9+(默认) |
| 实时系统(超低延迟、超大内存) | ZGC / Shenandoah | JDK 17+ |
| 性能测试、短期任务 | Epsilon | JDK 11+ |
3. 关键优化原则
- 避免 Full GC:Full GC 是性能杀手,需通过调整堆大小、分代比例、晋升阈值等减少 Full GC 触发。
- 控制年轻代大小:年轻代过大导致 Minor GC 延迟长,过小导致对象频繁晋升到老年代,触发 Major GC。
- 大对象处理:通过
-XX:PretenureSizeThreshold控制大对象直接进入老年代,避免年轻代复制开销。 - 监控 GC 状态:通过
jstat、jvisualvm等工具监控 GC 次数、STW 时间,针对性优化。
JVM 垃圾回收的核心是 “因地制宜”—— 根据应用的内存规模、并发量、性能目标选择合适的回收器和参数,无需追求 “最先进”,只需 “最适合”。
五、jvm中频繁执行full gc的原因及分析
首先需区分 “真正的 Full GC”(回收年轻代 + 老年代 + 元空间)和 “老年代 GC(Major GC)”,但实际监控中两者常被统称为 “Full GC”(如 jstat 中 FGCT 统计 Full GC 次数)。常见触发条件:
- 老年代空间不足(最主要原因);
- 元空间(Metaspace)不足(JDK 8+);
- 年轻代晋升到老年代时,老年代剩余空间不足以容纳;
- 调用
System.gc()(手动触发,JVM 可忽略,但频繁调用会生效); - CMS 回收器并发失败(Concurrent Mode Failure)或晋升失败(Promotion Failure);
- 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 的核心步骤(实操指南)
-
收集 GC 日志:添加 JVM 参数开启 GC 日志,记录关键信息:
bash
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:gc.log重点关注:Full GC 触发时间、老年代 / 元空间使用情况、回收前后内存变化、STW 时间。
-
分析内存使用趋势:
- 用
jstat -gc <pid> 1000实时监控堆内存变化(每 1 秒输出一次),关注O->OU(老年代已使用)是否持续上升; - 用
jvisualvm或 Grafana(结合 Prometheus)绘制老年代 / 元空间使用率曲线,判断是 “泄漏型”(持续上升)还是 “配置不当型”(波动频繁)。
- 用
-
生成堆 Dump 分析泄漏:
- 若老年代使用率持续上升,生成堆 Dump 用 MAT 分析,定位内存泄漏点(参考 “内存泄漏” 排查点);
- 重点关注静态集合、ThreadLocal、未关闭的资源等高频泄漏场景。
-
检查 JVM 参数配置:
- 堆内存分配:
-Xmx/-Xms是否过小,-XX:NewRatio(年轻代 / 老年代比例)、-XX:SurvivorRatio(Eden/Survivor 比例)是否合理; - 回收器参数:CMS 的
CMSInitiatingOccupancyFraction、ConcGCThreads,G1 的MaxGCPauseMillis、InitiatingHeapOccupancyPercent是否合理; - 元空间参数:
-XX:MaxMetaspaceSize是否过小,是否开启CMSClassUnloadingEnabled。
- 堆内存分配:
-
排查业务代码:
- 搜索
System.gc()调用; - 检查大对象创建、缓存使用(是否有过期淘汰)、资源关闭(流、连接)、ThreadLocal 使用(是否
remove())。
- 搜索
三、优化建议(针对性解决)
-
调整内存分配:
- 增大堆内存:
-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 再晋升)。
- 增大堆内存:
-
修复内存泄漏:
- 缓存添加过期机制:如 Guava Cache 配置
expireAfterWrite(10, TimeUnit.MINUTES),或使用 Redis 缓存替代本地静态集合; - 资源必须关闭:
finally中关闭流、连接(或用 try-with-resources 语法); - ThreadLocal 手动
remove():在请求结束或线程池任务执行完毕后,调用threadLocal.remove(); - 注销监听器 / 回调函数:不再使用时主动移除。
- 缓存添加过期机制:如 Guava Cache 配置
-
优化 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()调用)。
- CMS 优化:
-
更换合适的回收器:
- 大内存(>4GB):改用 G1 回收器(JDK 9+ 默认),避免 CMS 的内存碎片问题;
- 超大内存(>16GB):改用 ZGC/Shenandoah(JDK 17+ 转正),超低延迟且无碎片;
- 吞吐量优先场景:使用 Parallel Scavenge + Parallel Old,避免 CMS 的并发开销。
-
优化业务代码:
- 避免频繁创建大对象:分片处理大数据、复用对象(如使用对象池);
- 减少长存活对象:如会话数据设置合理的过期时间,避免长期驻留;
- 动态类加载优化:避免频繁热部署、限制动态代理类的生成数量。
四、总结
频繁 Full GC 的核心原因可归纳为: “老年代 / 元空间的‘进’大于‘出’” (“进” 是对象晋升 / 类加载,“出” 是 GC 回收)。排查时需先通过 GC 日志和监控工具判断是 “配置问题” 还是 “代码问题”:
- 若 Full GC 后内存释放较多,多是 内存分配 / 参数配置不当;
- 若 Full GC 后内存释放极少,多是 内存泄漏 / 对象存活时间过长。
优化的核心思路是:减少对象进入老年代的速度 + 提高老年代对象的回收效率,最终平衡吞吐量和延迟,避免 Full GC 频繁触发。