JVM垃圾回收算法(GC)的核心目标是自动识别并回收不在被引用的对象内存,避免内存泄漏或者溢出,底层依赖多种垃圾回收算法,不同算法在识别垃圾,回收内存,性能开销上各有侧重,实际GC器比如G1,ZGC都是基于这些基础的组合优化
一、前置:如何判断对象是垃圾?
JVM如何判定一个对象是否该被回收?核心标准是对象是否可达
- 以GC Roots为起点(如果虚拟机站中局部变量,方法区中类静态变量,本地方发展中JNI引用等),遍历对象引用链
- 如果一个对象到GC Roots没有任何可达路径及时不可达,就标记为垃圾对象,等待回收
二、核心垃圾回收算法
1.标记-清除算法(Mark-Sweep):最基础的算法
原理
分为两步执行:
- 标记阶段:遍历所有对象,标记出可达的存活对象(未标记的就是视为垃圾)
- 清除阶段:遍历堆内存,直接回收所有未标记的垃圾对象,释放内存空间
示意图
标记前:[存活A][垃圾B][存活C][垃圾D][存活E]
标记后:[存活A(标记)][垃圾B(未标)][存活C(标记)][垃圾D(未标)][存活E(标记)]
清除后:[存活A][空闲][存活C][空闲][存活E]
优缺点
- 优点:实现简单,不需要移动对象,执行效率高(标记和清除过程直接)
- 缺点:
- 内存碎片:回收产生大量不连续的空闲内存块,如果需要分配大对象,可能会因为没有足够的内存空间触发 Full GC;
- 效率不稳定:堆中的对象越多,标记和清除的遍历时间就越长,GC停顿时间肯呢个随着堆大小增长而增加
适用场景
早起GC器(比如Serial GC)的老年代实现,对内存连续性要求不高,对象存活率低的场景
2.复制算法(Copying):解决碎片问题的高效方案
原理
基于存活对象少,垃圾多的场景优化,核心就是分块复制和移动对象
- 将堆内存划分为两个大小相等的区域,每次只使用其中一块区域
- 标记阶段:标记From区中所有存活对象
- 复制阶段:将所有存活对象赋值到另一个空闲区域,在复制后对象在To区中是连续排列的
- 切换阶段:清空From区,将From区和To区角色互换一下(下次GC的时候使用新的From区)
示意图
初始状态:From区[存活A][垃圾B][存活C][垃圾D],To区[空闲][空闲][空闲][空闲]
标记+复制后:To区[存活A][存活C][空闲][空闲],From区[空闲][空闲][空闲][空闲]
角色互换:新From区[存活A][存活C][空闲][空闲],新To区[空闲][空闲][空闲][空闲]
优缺点
- 优点
- 无内存碎片:存活对象复制后连续排列,后续分配大对象时效率高;
- 回收效率高:只复制存活对象,若存活对象占比低(如新生代 90% 以上是临时对象),复制开销极小;
- 缺点
- 内存利用率低:堆内存被划分为两半,实际可用内存仅为总堆的 50%;
- 不适合存活对象多的场景:若存活对象占比高(如老年代),复制开销会急剧增加。
适用场景
新生代 GC(如 Serial GC、Parallel Scavenge GC),因为新生代对象生命周期短、存活占比低,契合复制算法的优势。
- 标记 - 整理算法(Mark-Compact):兼顾连续与利用率
原理
结合 "标记 - 清除" 和 "复制" 的优点,解决老年代存活对象多的问题,分三步:
- 标记阶段:与标记 - 清除一致,标记所有可达的存活对象;
- 整理阶段:将所有存活对象向堆内存的一端移动,紧凑排列;
- 清除阶段:直接清理存活对象边界之外的所有垃圾内存(无需遍历所有垃圾)。
示意图
标记前:[存活A][垃圾B][存活C][垃圾D][存活E] 标记后:[存活A(标记)][垃圾B(未标)][存活C(标记)][垃圾D(未标)][存活E(标记)] 整理后:[存活A][存活C][存活E][空闲][空闲]
优缺点
- 优点
- 无内存碎片:存活对象紧凑排列,内存连续性好;
- 内存利用率高:无需划分两半区域,全部堆内存均可使用;
- 缺点
- 额外的移动开销:需要移动存活对象,并更新所有引用该对象的指针(耗时);
- 停顿时间更长:整理阶段比标记 - 清除的清除阶段更耗时,适合对停顿不敏感的场景。
适用场景
老年代 GC(如 Serial Old GC、Parallel Old GC),因为老年代对象生命周期长、存活占比高,复制算法效率低,标记 - 清除会产生碎片,而标记 - 整理是最优选择。
- 分代收集算法(Generational Collection):实际 GC 器的核心逻辑
原理
不是独立算法,而是基于 "对象生命周期不同" 的分代策略,组合上述三种基础算法:
- 分代划分 :将堆内存分为「新生代」和「老年代」(部分 GC 器如 G1 还有「永久代 / 元空间」,但元空间不涉及堆内存回收);
- 新生代:存储新创建的对象,生命周期短、存活占比低(约 1%~10% 存活);
- 老年代:存储经过多次 GC 仍存活的对象,生命周期长、存活占比高(约 90% 以上存活);
- 算法组合 :
- 新生代:采用「复制算法」(高效、无碎片,契合低存活占比);
- 进一步细分:Eden 区(80%)+ 两个 Survivor 区(From/To 各 10%),每次只使用 Eden 和一个 Survivor,存活对象复制到另一个 Survivor,多次存活后晋升到老年代;
- 老年代:采用「标记 - 清除」或「标记 - 整理」(兼顾利用率和连续性,契合高存活占比);
- 新生代:采用「复制算法」(高效、无碎片,契合低存活占比);
- 回收触发 :
- 新生代 GC(Minor GC):Eden 区满时触发,只回收新生代,停顿时间短;
- 老年代 GC(Major GC/Full GC):老年代满或元空间不足时触发,回收新生代 + 老年代,停顿时间长。
优缺点
- 优点:针对性优化,兼顾效率和内存利用率,是目前所有主流 GC 器(如 Parallel GC、G1、ZGC)的基础;
- 缺点:分代逻辑复杂,需要维护对象年龄、晋升规则等,依赖 JVM 对对象生命周期的精准判断
适用场景
所有现代 JVM(HotSpot 等)的默认 GC 策略,几乎覆盖所有应用场景(从桌面应用到服务器应用)。
- 分区收集算法(Region-Based Collection):大堆内存的优化方案
原理
为解决 "大堆内存下 Full GC 停顿过长" 的问题(如堆内存达数十 GB),核心是「将堆划分为多个小 Region,按需回收」:
- 堆内存被划分为多个大小相等的 Region(如 G1 中 Region 大小为 1~32MB,可配置);
- 每个 Region 可动态标记为「新生代(Eden/Survivor)」或「老年代」,无需固定分代比例;
- GC 时优先回收「垃圾占比高的 Region」(G1 称为 "优先回收"),避免全堆扫描;
- 结合标记 - 整理算法:对单个 Region 内的存活对象进行整理,保证内存连续性。
优缺点
- 优点
- 降低停顿时间:只回收部分 Region,而非全堆,大堆内存下停顿可控;
- 灵活分代:Region 角色可动态调整,适配不同对象分布场景;
- 缺点
- Region 管理复杂:需要维护每个 Region 的状态(空闲、新生代、老年代、垃圾占比等);
- 额外开销:Region 之间的引用需要通过 "卡表"(Card Table)追踪,增加内存和计算开销。
适用场景
大堆内存场景(如堆内存 ≥ 8GB),主流 GC 器如 G1 GC、ZGC、Shenandoah GC 均基于此算法。
三、主流 GC 器与算法对应关系
| GC 器 | 适用代际 | 核心算法组合 | 特点 |
|---|---|---|---|
| Serial GC | 新生代 + 老年代 | 新生代:复制算法;老年代:标记 - 整理 | 单线程,停顿长,适合小堆 |
| Parallel Scavenge GC | 新生代 | 复制算法 | 多线程吞吐量优先 |
| Parallel Old GC | 老年代 | 标记 - 整理 | 多线程吞吐量优先 |
| CMS GC(废弃) | 老年代 | 标记 - 清除(并发标记) | 低停顿,有内存碎片 |
| G1 GC | 全堆(Region) | 分区收集 + 标记 - 整理 + 复制 | 平衡停顿与吞吐量,大堆友好 |
| ZGC | 全堆(Region) | 分区收集 + 标记 - 整理(并发整理) | 超低停顿(毫秒级),超大堆支持 |
| Shenandoah GC | 全堆(Region) | 分区收集 + 标记 - 整理(并发整理) | 低停顿,跨平台支持 |
四、核心总结
- 基础算法定位 :
- 标记 - 清除:简单但有碎片,适合临时场景;
- 复制算法:高效无碎片但利用率低,适合低存活占比(新生代);
- 标记 - 整理:无碎片高利用率但有移动开销,适合高存活占比(老年代);
- 实际应用逻辑 :
- 分代收集是基础策略,结合复制 + 标记 - 整理,覆盖大多数场景;
- 分区收集是大堆优化,通过 Region 按需回收,解决大堆停顿问题;
- 选择原则 :
- 小堆(<4GB):Serial GC / Parallel GC(吞吐量优先);
- 中堆(4~16GB):G1 GC(平衡停顿与吞吐量);
- 大堆(>16GB):ZGC / Shenandoah GC(超低停顿)。