JVM中的垃圾回收算法和垃圾回收器

一、垃圾回收算法

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

分为两个阶段:

标记阶段:从根对象(如栈引用、静态变量等)出发,遍历所有可达对象并标记为 "存活"。

清除阶段:遍历整个内存区域,回收所有未被标记的对象(即垃圾),释放其占用的内存。

**优点:**实现简单,不需要移动对象。

**缺点:**效率低,记和清除过程都需要遍历所有对象,耗时较长,尤其在对象数量多的时候。

回收后会产生大量不连续的内存碎片,可能导致后续大对象无法分配内存(即使总空间足够)。

2.复制算法(Copying)

复制算法可以解决标记-清除算法的效率和内存碎片问题。

**原理:**将内存划分为两个大小相等的区域(如 From 区和 To 区),每次只使用其中一个区域(From 区)。当 From 区内存不足时,标记所有存活对象,将其复制到另一个未使用的区域(To 区),并按顺序排列(避免碎片);清除 From 区所有对象,然后交换 From 区和 To 区的角色,重复使用。

优点:效率高:只处理存活对象,复制过程中自然消除内存碎片.

适合存活对象少的场景(如新生代,因为大部分对象朝生夕灭)。

**缺点:**内存利用率低,需要预留一半内存作为复制区域,实际可用内存仅为总容量的一半。

不适合存活对象多的场景(复制成本高)。

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

结合了 "标记 - 清除" 和 "复制" 的思想,分为三个阶段:

标记阶段:同标记 - 清除算法,标记所有存活对象。

整理阶段:将所有存活对象向内存一端移动,按顺序排列。

清除阶段:回收边界外的所有内存(即未标记的垃圾)。

**优点:**解决了标记 - 清除算法的内存碎片问题。

内存利用率高于复制算法(无需预留一半空间)。

缺点:效率较低,比标记 - 清除多了 "整理" 阶段(移动对象并更新引用),耗时更长。

适合存活对象多的场景(如老年代,对象存活率高)。

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

不是独立的算法,而是根据对象的生命周期(存活时间)将内存划分为不同区域(如新生代、老年代),结合上述算法针对性处理。

新生代:对象存活时间短(大部分创建后很快被回收),采用 复制算法(效率高,适合低存活率场景)。新生代通常进一步分为 Eden 区和两个 Survivor 区(From/To),比例一般为 8:1:1,减少内存浪费。

老年代 :对象存活时间长(多次回收后仍存活),采用 标记 - 清除标记 - 整理算法(适合高存活率场景,避免复制成本)。

优点:充分利用不同代对象的特性,提高垃圾回收效率,是目前主流 JVM(如 HotSpot)的默认策略。

二、延迟和吞吐量

先理解两个概念延迟和吞吐量

**延迟:**这里特指 "GC 停顿时间"(STW,Stop-The-World),即垃圾回收时用户线程被暂停的时间。延迟优先的目标是尽可能缩短单次 GC 的停顿时间(比如控制在 10ms 以内),避免用户感知到卡顿(如 Web 请求超时、游戏卡顿)。延迟小,GC 停顿时间短,应用程序可以更快响应用户请求。

吞吐量(Throughput):指 "用户线程运行时间占总时间的比例"(吞吐量 = 用户线程时间 /(用户线程时间 + GC 时间))。吞吐量优先的目标是尽可能让用户线程占用更多 CPU 时间,减少 GC 的总耗时占比(比如控制 GC 时间占比不超过 1%),适合后台计算、批处理等不关注实时响应的场景。

延迟优先的策略会降低吞吐量,吞吐量优先会增加延迟。

1.为什么延迟增加会保证吞吐量?

"延迟增加会保证吞吐量" 的核心逻辑是:通过允许单次 GC 的停顿时间(延迟)适当增加,减少 GC 的总次数和总耗时,从而让用户线程获得更多连续运行的时间,最终提高整体吞吐量。这里的 "延迟增加" 特指 "单次 GC 的 STW(Stop-The-World)时间变长",而吞吐量的提升源于对 "GC 总开销" 的优化。

**1.1.**GC 操作本身是有 "固定开销" 的(如初始化 GC 线程、遍历根对象、更新内存管理元数据等),这些开销与回收的垃圾量无关。若 GC 触发过于频繁(每次只回收少量垃圾),这些固定开销会被反复叠加,导致 GC 总时间急剧增加;而适当延长单次 GC 的延迟(让 GC 一次处理更多垃圾),可减少 GC 次数,从而降低固定开销的总占比。比如

**1.2.**减少 GC 对用户线程的 "干扰成本",频繁的 GC 不仅增加总时间,还会通过 "线程切换" 干扰用户线程的连续运行:频繁的 GC 不仅增加总时间,还会通过 "线程切换" 干扰用户线程的连续运行;每次 GC 触发时,CPU 需要从 "用户线程" 切换到 "GC 线程"(保存上下文、加载 GC 线程状态),GC 结束后,再从 "GC 线程" 切换回 "用户线程"(恢复上下文)。这些切换操作会消耗 CPU 资源,且会打断用户线程的缓存局部性(如 CPU 缓存中的数据因切换失效,需要重新加载),导致用户线程运行效率下降。而 "单次延迟增加、次数减少" 的策略,能减少线程切换的频率,让用户线程获得更长的连续运行时间:减少切换成本,CPU 更多时间用于实际业务计算;保持更好的缓存局部性,用户线程执行效率更高

**1.3.**避免 "碎片化导致的额外 GC 开销",吞吐量优先的 GC(如 Parallel Old)在老年代采用 "标记 - 整理算法",虽然单次整理会增加延迟(需要移动对象、更新引用),但能彻底消除内存碎片。内存碎片的危害是:即使总内存充足,大对象也可能因找不到连续空间而触发额外的 Full GC(例如,10GB 堆中有 2GB 碎片,但一个 3GB 的大对象仍无法分配,必须触发 GC 整理碎片)。这种 "为了处理碎片而额外触发的 GC" 会显著增加总 GC 时间,降低吞吐量。而允许单次延迟增加(接受整理阶段的耗时),可避免碎片累积,减少这类 "不可控的额外 GC",从而保证总 GC 时间的稳定性,间接提升吞吐量。

三、垃圾回收器

一、JDK 1.3 及之前:单线程回收器(Serial GC 时代)

这一阶段仅支持Serial GC,是 JVM 最早的垃圾回收器,无专门的新生代 / 老年代回收器划分,而是同一回收器处理整堆。

**核心回收器:**Serial GC

适用区域:同时负责新生代和老年代。

新生代处理:

算法:复制算法(单线程标记 + 复制存活对象)。

特点:回收时暂停所有用户线程(STW),因新生代对象存活率低,单次回收快但频率高。

老年代处理

算法:标记 - 整理算法(单线程标记 + 移动存活对象)。

特点:老年代对象存活率高,单次回收耗时较长(STW 时间比新生代长),频率低。

新生代与老年代回收器的区别:

算法不同:新生代用复制(低存活场景高效),老年代用标记 - 整理(高存活场景避免碎片)。

耗时与频率:新生代回收频率高、耗时短;老年代频率低、耗时长(因对象多且需整理)。

线程模型:均为单线程(无并行 / 并发能力),STW 影响随堆大小增加而显著。

二、JDK 1.4:多线程吞吐量优先回收器登场

随着多核 CPU 普及,单线程 Serial GC 效率不足,JDK 1.4 引入Parallel Scavenge(新生代多线程回收器),老年代仍依赖 Serial Old(Serial GC 的老年代部分)。

**核心回收器组合:**Parallel Scavenge(新生代) + Serial Old(老年代)

**新生代:**Parallel Scavenge

算法:复制算法(多线程并行标记 + 复制,利用多核优势)。

特点:专注 "吞吐量"(用户线程运行时间占比),可通过 -XX:GCTimeRatio 控制 GC 时间占比;STW 时间比 Serial GC 短(多线程并行)。

**老年代:**Serial Old

算法:标记 - 整理算法(单线程执行)。

特点:单线程处理高存活对象,STW 时间长(成为吞吐量瓶颈)。

新生代与老年代回收器的区别:

线程模型:新生代多线程并行,老年代单线程串行(核心区别)。

效率瓶颈:新生代因多线程效率高,老年代单线程拖慢整体吞吐量。

适用场景:新生代适合低存活对象快速回收,老年代仅能处理小堆(因单线程限制)。

三、JDK 5:老年代并行回收器完善(Parallel GC 成熟)

JDK 5 引入Parallel Old(老年代多线程回收器),与 Parallel Scavenge 组成完整的 "吞吐量优先" 组合(Parallel GC)。

**核心回收器组合:**Parallel Scavenge(新生代) + Parallel Old(老年代)

**新生代:**Parallel Scavenge

算法:复制算法(多线程并行),与 JDK 1.4 一致,优化了线程调度。

**老年代:**Parallel Old

算法:标记 - 整理算法(多线程并行执行标记和整理)。

特点:解决了 Serial Old 单线程瓶颈,全堆多线程并行回收,吞吐量大幅提升。

新生代与老年代回收器的区别

算法差异:新生代复制(低存活场景高效),老年代标记 - 整理(高存活场景避免复制成本)。

线程协同:均为多线程并行,回收时同步 STW(无并发阶段),适合堆内存中等规模(GB 级)。

调优重点:新生代关注 "Eden 区与 Survivor 区比例",老年代关注 "整理效率"(避免大对象导致的频繁 Full GC)。

四、JDK 6:低延迟并发回收器(CMS 登场)

对于响应时间敏感的应用(如 Web 服务),Parallel GC 的长 STW 不可接受,JDK 6 引入CMS(Concurrent Mark Sweep) 作为老年代低延迟回收器,新生代搭配ParNew(专为 CMS 优化的多线程回收器)。

**核心回收器组合:**ParNew(新生代) + CMS(老年代)

**新生代:**ParNew

算法:复制算法(多线程并行,与 Parallel Scavenge 类似)。

特点:唯一能与 CMS 配合的新生代回收器(支持 "并发模式失败" 时的分配担保);STW 时间短(多线程),频率高。

**老年代:**CMS

算法:标记 - 清除算法(大部分阶段与用户线程并发)。

核心流程:初始标记(STW,快)→ 并发标记(与用户线程并行)→ 重新标记(STW,短)→ 并发清除(与用户线程并行)。

特点:STW 时间极短(仅初始和重新标记阶段),适合低延迟场景;但产生内存碎片,并发阶段占用 CPU 资源。

新生代与老年代回收器的区别

线程模型:新生代并行 STW(所有 GC 线程同时工作,暂停用户线程);老年代并发(GC 线程与用户线程同时运行,仅短时间 STW)。

算法目标:新生代追求 "快速回收短生命周期对象"(复制算法无碎片);老年代追求 "低延迟"(标记 - 清除牺牲碎片换速度)。

缺陷互补:ParNew 无碎片但需 STW;CMS 低延迟但有碎片,需定期用 Serial Old 进行 Full GC 整理(称为 "CMS 退化")。

五、JDK 7:大堆回收器 G1 实验性引入

传统回收器(如 CMS)对大堆(10GB 以上)处理效率下降(碎片、并发成本高),JDK 7 引入G1(Garbage-First),采用 "区域化" 设计,弱化新生代与老年代的物理划分。

**核心回收器:**G1(整堆回收,动态代划分)

G1 将堆划分为多个大小相等的 Region(通常 1-32MB),每个 Region 可动态扮演 Eden/Survivor/Old 角色(逻辑上的新生代 / 老年代)。

  • 逻辑新生代(Eden + Survivor Region):
    • 算法:复制算法(并行标记 + 复制存活对象到新 Region)。
    • 特点:回收时优先处理垃圾多的 Eden Region,STW 时间短(类似 Minor GC)。
  • 逻辑老年代(Old Region):
    • 算法:标记 - 整理算法(结合复制,回收时将存活对象复制到新 Region 并整理)。
    • 特点:通过 "混合回收"(Mixed GC)处理老年代,即同时回收部分 Old Region 和新生代,避免 Full GC;无内存碎片。

新生代与老年代(逻辑)的区别

物理划分:无固定边界,通过 Region 动态切换角色(核心区别于传统分代)。

回收策略:新生代回收(类似 Minor GC)仅处理 Eden 和 Survivor Region;老年代通过 "混合回收" 逐步处理,避免一次性回收全量老年代导致的长 STW。

目标控制 :可通过 -XX:MaxGCPauseMillis 设定目标停顿时间,G1 会优先选择垃圾占比高的 Region 回收(兼顾吞吐与延迟)。

六、JDK 8:G1 正式启用,CMS 仍为主流

JDK 8 中 G1 从实验性变为正式可用,但默认回收器仍是 Parallel GC(Parallel Scavenge + Parallel Old),CMS 仍广泛用于低延迟场景。

核心回收器组合:

默认:Parallel Scavenge(新生代) + Parallel Old(老年代)(吞吐量优先)。

可选:ParNew(新生代) + CMS(老年代)(低延迟);G1(整堆,平衡吞吐与延迟)。

新生代与老年代区别(同 JDK 6/7):

Parallel GC:新生代多线程复制,老年代多线程标记 - 整理,均并行 STW。

CMS 组合:新生代并行 STW,老年代并发标记 - 清除。

G1:逻辑代划分,动态 Region 管理,回收策略灵活。

七、JDK 9:G1 成为默认回收器

JDK 9 中 G1 替代 Parallel GC 成为默认回收器,更适合中等规模堆(10-100GB),兼顾吞吐量和延迟。

核心回收器:G1(默认)

新生代(逻辑):Eden/Survivor Region 占比动态调整(默认新生代占堆 50%,可自动优化)。

老年代(逻辑):通过混合回收逐步清理,减少 Full GC 频率。

新生代与老年代区别

与 JDK 7 的 G1 一致,核心是 "动态 Region 管理" 和 "优先回收垃圾多的区域",弱化物理代划分,更关注停顿时间控制。

八、JDK 11+:低延迟大堆回收器崛起(ZGC、Shenandoah)

JDK 11 及之后,针对 TB 级堆和毫秒级停顿需求,引入 ZGCShenandoah,彻底弱化 "新生代 / 老年代" 的传统分代模型。

1. ZGC(JDK 11 实验性,JDK 14 生产就绪)
  • 设计理念:整堆 Region 划分,无严格新生代 / 老年代,通过 "颜色指针" 和 "读屏障" 实现几乎全并发回收。
  • 处理方式:所有对象(无论新老)均在 Region 中管理,回收时并发标记、并发移动,STW 仅发生在初始标记和最终标记(<1ms)。
  • 新老对象区别:无明确代划分,但内部通过 "年龄" 跟踪对象存活时间,优先回收短期对象(类似新生代逻辑),但无物理隔离。
2. Shenandoah(JDK 12 实验性,JDK 14 生产就绪)
  • 设计理念:整堆 Region 划分,通过 "转发指针" 和 "写屏障" 实现并发整理,支持 TB 级堆。
  • 处理方式:回收过程(标记、整理)几乎全并发,STW 时间极短(<10ms);无严格代划分,通过 "年龄" 优化回收顺序。
新生代与老年代的区别(ZGC/Shenandoah)
  • 无物理代划分:不再区分固定的新生代 / 老年代区域,对象按存活时间动态管理(核心区别于传统 GC)。
  • 回收策略统一:新对象和老对象用相同的并发算法处理,避免传统分代中 "新生代回收依赖老年代状态" 的耦合问题。
  • 优化方向:通过 "年龄过滤" 优先回收短期对象(类似新生代逻辑),但无独立的复制 / 标记阶段,效率更高。

九、JDK 14 及之后:CMS 废弃,低延迟 GC 主导

  • JDK 14:CMS 被标记为废弃(维护成本高),ZGC 和 Shenandoah 正式进入生产阶段。
  • JDK 17(LTS):完全移除 CMS,G1 仍是默认 GC(适合中等堆),ZGC/Shenandoah 成为超大堆(TB 级)和低延迟场景的首选。
JDK 版本 主流回收器组合 新生代回收器特点 老年代回收器特点 核心区别
1.3 及之前 Serial GC 单线程复制,频率高、STW 短 单线程标记 - 整理,频率低、STW 长 算法不同,均单线程
1.4 Parallel Scavenge + Serial Old 多线程复制,吞吐量优先 单线程标记 - 整理,效率低 新生代多线程,老年代单线程
5-8(默认) Parallel Scavenge + Parallel Old 多线程复制,吞吐量优先 多线程标记 - 整理,吞吐量优先 均多线程并行,算法不同(复制 vs 标记 - 整理)
6-13(可选) ParNew + CMS 多线程复制,支持 CMS 协作 并发标记 - 清除,低延迟、有碎片 新生代并行 STW,老年代并发(核心是线程模型差异)
7+(G1) G1 逻辑新生代,Region 复制,动态调整 逻辑老年代,Region 标记 - 整理,混合回收 无固定物理代,动态 Region 管理,优先回收垃圾多的区域
11+(ZGC/Shenandoah) ZGC / Shenandoah 无物理新生代,并发回收短期对象 无物理老年代,并发回收长期对象 无代划分,统一并发算法,STW 极短

四、CMS垃圾回收器

CMS(Concurrent Mark Sweep,并发标记清除)是 JVM 中一款以低延迟为核心目标的老年代垃圾回收器,专为响应时间敏感的应用(如 Web 服务、实时交易系统)设计。它通过 "并发标记" 和 "并发清除" 阶段减少 STW(Stop-The-World,暂停用户线程)时间,尽可能降低 GC 对业务的影响。

1.CMS 的核心设计目标

最小化 STW 时间:通过让 GC 线程与用户线程 "并发执行"(大部分阶段不暂停用户线程),将 STW 控制在毫秒级,避免用户感知卡顿。

适配老年代 :主要负责老年代回收,新生代通常搭配ParNew 回收器(唯一能与 CMS 协作的新生代回收器)。

2.CMS 的回收流程(老年代)

CMS 的回收过程分为4 个主要阶段,其中仅 2 个阶段需要 STW,其余阶段与用户线程并发执行:

1. 初始标记(Initial Mark)------ STW(极短)
  • 目标:标记 "根对象直接引用的老年代对象"(如栈中引用、静态变量指向的老年代对象)。
  • 特点:仅扫描根对象的直接关联,不遍历整个对象图,因此 STW 时间极短(通常几十微秒到几毫秒)。
2. 并发标记(Concurrent Mark)------ 与用户线程并发
  • 目标:从初始标记的对象出发,遍历整个老年代的对象引用链,标记所有存活对象。
  • 特点:GC 线程与用户线程同时运行,不暂停业务;但用户线程可能修改对象引用(如创建新对象、断开引用),导致部分对象标记不准确(后续阶段修正)。
  • 耗时:最长的阶段(取决于老年代大小和对象数量),但不影响用户线程执行。
3. 重新标记(Remark)------ STW(较短)
  • 目标:修正并发标记期间因用户线程操作导致的 "标记偏差"(如对象被删除或新增引用)。
  • 实现:通过 "卡表"(记录并发阶段被修改的内存区域)快速定位需重新扫描的对象,避免全堆遍历。
  • 特点:STW 时间比初始标记长,但远短于并发标记(通常几毫秒到几十毫秒)。
4. 并发清除(Concurrent Sweep)------ 与用户线程并发
  • 目标:回收所有未被标记的垃圾对象(清理内存),释放空间。
  • 特点 :GC 线程与用户线程并发执行,不暂停业务;采用 "标记 - 清除" 算法,不移动存活对象,因此会产生内存碎片

3.CMS 的优缺点

优点:
  1. 低延迟:仅初始标记和重新标记阶段 STW,总停顿时间极短(通常 < 100ms),适合响应敏感场景。
  2. 并发执行:大部分工作与用户线程并行,不阻塞业务流程,用户感知弱。
缺点:
  1. 内存碎片:采用 "标记 - 清除" 算法,不整理存活对象,长期运行会产生大量碎片,可能导致大对象无法分配内存(即使总空间充足),最终触发 Full GC(使用 Serial Old 回收器进行标记 - 整理,STW 时间长)。
  2. CPU 资源消耗高:并发阶段 GC 线程与用户线程竞争 CPU,若 CPU 核心少(如 < 4 核),会导致用户线程运行变慢(吞吐量下降)。
  3. 浮动垃圾:并发清除阶段用户线程新产生的垃圾(未被标记)无法被本次回收,需留到下次 GC,因此老年代需预留部分空间(通常 10%-20%),否则可能因内存不足触发 "并发模式失败"(强制 STW,改用 Serial Old 回收)。
  4. 不支持增量回收:并发标记和清除阶段不可中断,若老年代过大,可能导致长时间占用 CPU。

4.CMS 的适用场景

  • 响应时间优先:如 Web 服务、API 接口、实时交易系统(需快速响应用户请求)。
  • 中小规模老年代:堆内存不宜过大(建议 < 10GB),否则并发标记 / 清除阶段耗时过长,CPU 占用过高。
  • CPU 核心充足:需预留足够 CPU 支持 GC 线程(通常建议 CPU 核心数≥4),避免并发阶段影响业务。

5.CMS 的调优参数(常用)

  • -XX:+UseConcMarkSweepGC:启用 CMS 回收器(JDK 9 后需额外配置,JDK 14 废弃)。
  • -XX:ConcGCThreads:设置并发 GC 线程数(默认约为 CPU 核心数的 1/4)。
  • -XX:CMSInitiatingOccupancyFraction:老年代使用率达到该阈值时触发 CMS(默认 68%,可降低该值减少并发模式失败风险)。
  • -XX:+UseCMSCompactAtFullCollection:Full GC 时进行内存整理(解决碎片,默认开启)。
  • -XX:CMSFullGCsBeforeCompaction:多少次 Full GC 后进行一次整理(默认 0,即每次 Full GC 都整理)。

6.CMS 的历史地位

  • 诞生:JDK 6 正式引入,解决了传统回收器 STW 过长的问题,成为低延迟场景的首选。
  • 衰落:因内存碎片、CPU 消耗高、维护成本大,JDK 14 被标记为废弃,JDK 17 完全移除。
  • 替代者:被 G1、ZGC、Shenandoah 等回收器取代(这些回收器兼顾低延迟和无碎片,支持更大堆)。
相关推荐
江畔柳前堤8 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
Forever Nore10 小时前
LeetCode 4 寻找两个正序数组的中位数 - 二分
算法·leetcode
罗西的思考11 小时前
【OpenClaw具身硬件】MiniClaw 阅读笔记---(1)基础
人工智能·算法·机器学习
蛋先生DX12 小时前
大模型参数存储格式揭秘:BF不是男朋友
深度学习·算法·llm
爱跳舞的烤冷面12 小时前
自学嵌入式第22天(数据结构——哈希)
数据结构·算法·哈希算法
猎嘤一号13 小时前
博弈论(Game Theory)的理论、算法与工程
人工智能·算法·安全·博弈论
月光船幽幽13 小时前
影子模式下保护 logits 不被修改
人工智能·python·算法
马拉AI13 小时前
腾讯开源 Agent 记忆系统,AI“换对话就忘”的问题有了新解法(附安装使用教程)
人工智能·算法·开源·科研
地平线开发者15 小时前
征程6工具链模型X86推理方式说明
算法
地平线开发者15 小时前
【征程6】校准量化中HistogramObserver解析
算法