JVM 垃圾回收算法:原理、实现与适用场景
JVM 垃圾回收(GC)的核心目标是识别并回收不再被引用的对象内存,同时尽可能降低对应用性能的影响。其算法设计围绕 "如何高效判定垃圾" 和 "如何高效回收垃圾" 两大核心问题展开,下面从基础原理到进阶实现,深入拆解核心算法及衍生优化。
一、前置:垃圾判定的核心准则
所有 GC 算法的前提是 "识别垃圾",主流判定方式有两种:
1. 引用计数法(Reference Counting)
- 原理:为每个对象维护一个引用计数器,有引用指向时 + 1,引用失效时 - 1;计数器为 0 则判定为垃圾。
- 优点:实时性高,回收无延迟;实现简单。
- 致命缺陷 :无法解决循环引用(如 A 引用 B、B 引用 A,两者计数器均不为 0,但均无外部引用),JVM 未采用此方式(Python 等语言使用但需额外处理循环引用)。
2. 可达性分析算法(Reachability Analysis)
- 原理 :以 "GC Roots" 为起点,遍历对象引用链,不可达的对象判定为垃圾。
- GC Roots 核心类型 :
- 虚拟机栈(栈帧局部变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈(JNI)中引用的对象;
- JVM 内部的核心对象(如锁对象、系统类加载器)。
- 优势:解决循环引用问题,是 JVM(HotSpot)的核心判定方式。
- 注意:可达性分析需 "暂停所有用户线程"(STW,Stop The World),否则引用链会动态变化,导致判定错误。
二、核心垃圾回收算法(基础层)
基于可达性分析,JVM 衍生出 4 类核心回收算法,各有适用场景和性能特点:
1. 标记 - 清除算法(Mark-Sweep)
原理(两步走)
- 标记阶段:从 GC Roots 出发,标记所有可达对象;
- 清除阶段:遍历堆内存,回收所有未标记的对象,释放内存空间。
优缺点
| 优点 | 缺点 |
|---|---|
| 实现简单,无需移动对象 | 1. 内存碎片化严重(回收后产生大量不连续的内存碎片,大对象无法分配时触发 Full GC);2. 效率低(标记 + 清除均需遍历全堆,STW 时间长);3. 仅回收空间,未整理空间。 |
适用场景
仅适用于老年代(对象存活率高,移动成本高),但因碎片化问题,极少单独使用(早期 CMS 的初始标记 + 并发清除阶段基于此)。
2. 标记 - 复制算法(Mark-Copy)
原理(空间换时间)
- 将堆内存划分为大小相等的两块(From 区 / To 区),仅使用其中一块;
- 标记阶段:标记 From 区中可达对象;
- 复制阶段:将所有标记的对象复制到 To 区,按内存地址连续排列;
- 交换阶段:清空 From 区,交换 From/To 区角色,下次 GC 操作新的 From 区。
优化版(Appel 式分配)
HotSpot 针对新生代优化:将新生代分为 1 个 Eden 区 + 2 个 Survivor 区(默认比例 8:1:1),而非等大分区:
- 大部分对象在 Eden 区创建,GC 时标记 Eden+Survivor(From)的可达对象,复制到 Survivor(To);
- 若 To 区空间不足,直接晋升到老年代;
- 交换 From/To 区,重复此过程。
优缺点
| 优点 | 缺点 |
|---|---|
| 1. 无内存碎片(复制后对象连续排列);2. 回收效率高(仅处理存活对象,新生代存活率低);3. 分配内存时仅需移动指针,效率极高 | 1. 内存利用率低(需预留 To 区,Appel 式优化后仍损失 10%);2. 老年代存活率高,复制成本极高,不适用。 |
适用场景
新生代核心算法(如 Serial GC、ParNew GC、G1 的新生代回收),因新生代对象 "朝生夕死",复制成本远低于标记 - 清除。
3. 标记 - 整理算法(Mark-Compact)
原理(标记 + 移动 + 清除)
- 标记阶段:同标记 - 清除,标记可达对象;
- 整理阶段 :将所有标记的对象向内存一端压缩移动,保证连续排列;
- 清除阶段:清理边界外的所有内存空间。
优缺点
| 优点 | 缺点 |
|---|---|
| 1. 无内存碎片;2. 内存利用率 100%(无需预留空间);3. 适合存活率高的区域。 | 1. 移动对象成本高(需更新所有引用指向,STW 时间长);2. 整理过程需暂停所有用户线程,并发难度大。 |
适用场景
老年代核心算法(如 Serial Old、Parallel Old,G1 的老年代回收也包含整理逻辑),平衡了碎片化和内存利用率。
4. 分代收集算法(Generational Collection)
本质
并非独立算法,而是基于对象生命周期的分代策略,结合上述 3 种算法的优势:
- 新生代:对象存活率低 → 标记 - 复制算法;
- 老年代:对象存活率高、体积大 → 标记 - 清除 / 标记 - 整理算法;
- 永久代 / 元空间:极少回收(仅卸载无用类)。
核心优势
- 针对不同区域选择最优算法,降低整体 GC 开销;
- 新生代 Minor GC 频率高但耗时短,老年代 Major GC/Full GC 频率低但耗时长。
实现细节
- 新生代 GC(Minor GC):触发条件为 Eden 区满,回收 Eden+From Survivor,存活对象移至 To Survivor;
- 老年代 GC(Major GC):触发条件为老年代空间不足 / 晋升对象超过老年代剩余空间,或显式调用 System.gc ();
- 空间分配担保:Minor GC 前,JVM 检查老年代最大可用连续空间是否≥新生代所有对象总大小,若不足则触发 Full GC。
三、进阶算法:并发回收与区域化回收
随着 JVM 发展,基础算法无法满足高并发、低延迟需求,衍生出并发回收 和区域化回收的进阶设计:
1. 并发标记 - 清除(CMS,Concurrent Mark Sweep)
核心目标
降低老年代 GC 的 STW 时间,基于 "标记 - 清除" 做并发优化,分为 4 个阶段:
- 初始标记(STW):标记 GC Roots 直接关联的对象,耗时极短;
- 并发标记:遍历引用链,标记所有可达对象(与用户线程并发执行,无 STW);
- 重新标记(STW):修正并发标记期间因用户线程操作导致的标记遗漏(耗时短于初始标记);
- 并发清除:回收未标记对象(与用户线程并发执行,无 STW)。
优缺点
| 优点 | 缺点 |
|---|---|
| 1. 并发阶段无 STW,低延迟(适合响应时间敏感的场景);2. 老年代回收效率高。 | 1. 产生内存碎片(标记 - 清除的固有问题);2. 并发阶段占用 CPU 资源,影响应用吞吐量;3. 无法处理 "浮动垃圾"(并发清除阶段产生的新垃圾,需下次 GC 回收);4. 依赖标记 - 整理做碎片整理(CMS 失败时触发 Serial Old 的 Full GC,STW 极长)。 |
2. 垃圾优先(G1,Garbage-First)
核心设计
打破分代边界,将堆划分为多个大小相等的 Region(默认 1~32MB),每个 Region 可动态标记为 Eden、Survivor、Old、Humongous(存储大对象),结合 "标记 - 复制" 和 "标记 - 整理",核心是 "优先回收垃圾最多的 Region"。
核心流程(简化)
- 初始标记(STW):标记 GC Roots 关联的对象,暂停时间极短;
- 并发标记:遍历引用链,标记可达对象,计算每个 Region 的垃圾占比;
- 最终标记(STW):修正并发标记的遗漏,同时清理 Remembered Set(记录跨 Region 引用);
- 筛选回收(STW):按垃圾占比排序 Region,优先回收垃圾多的 Region(新生代 Region 用标记 - 复制,老年代 Region 用标记 - 整理)。
优势
- 可预测的 STW 时间(通过参数设置最大暂停时间);
- 无内存碎片(整理 + 复制);
- 兼顾吞吐量和延迟,适合大内存场景(如 16GB 以上堆)。
3. ZGC/Shenandoah(超低延迟 GC)
核心创新
基于 "染色指针" 和 "并发整理",实现几乎全程无 STW 的回收:
- 染色指针:将对象的 GC 状态(标记、移动等)存储在指针的高位比特位,无需修改对象本身;
- 并发移动:在用户线程访问对象时,通过指针转发机制动态更新引用,实现对象移动与用户线程并发;
- Region 化管理:与 G1 类似,但 Region 大小可动态调整,支持 TB 级堆内存。
核心优势
STW 时间稳定在毫秒级(甚至微秒级),适合超大规模内存、超低延迟场景(如金融、实时交易)。
四、算法对比与选型建议
| 算法 / 收集器 | 核心回收算法 | 适用区域 | STW 特点 | 适用场景 |
|---|---|---|---|---|
| Serial GC | 新生代:标记 - 复制;老年代:标记 - 整理 | 新生代 + 老年代 | 全程 STW,单线程 | 单核心、小堆内存(如客户端程序) |
| Parallel GC | 新生代:标记 - 复制;老年代:标记 - 整理 | 新生代 + 老年代 | 全程 STW,多线程 | 吞吐量优先(如后台批处理) |
| CMS | 老年代:并发标记 - 清除;新生代:ParNew(标记 - 复制) | 老年代 + 新生代 | 仅初始 / 重新标记 STW | 延迟优先(如 Web 应用) |
| G1 | 区域化:标记 - 复制 + 标记 - 整理 | 全堆(Region) | 可预测的短 STW | 大内存、兼顾吞吐量与延迟(如中间件) |
| ZGC/Shenandoah | 区域化:并发标记 + 并发整理 | 全堆(Region) | 几乎无 STW | 超大内存、超低延迟(如金融、云计算) |
五、关键优化点
- 卡表(Card Table):避免全堆扫描跨代引用(新生代对象引用老年代 / 老年代引用新生代),仅扫描脏卡,降低 GC 开销;
- Remembered Set:G1/ZGC 中记录跨 Region 引用,减少标记范围;
- TLAB(Thread Local Allocation Buffer):线程私有内存分配缓冲区,避免多线程分配内存竞争,提升新生代分配效率;
- 并发失败处理:CMS/G1 在并发阶段内存不足时,降级为 Serial Old 的 Full GC,需监控此类场景。
总结
JVM GC 算法的演进核心是平衡 "延迟(STW 时间)"、"吞吐量" 和 "内存利用率":
- 基础算法(标记 - 清除 / 复制 / 整理)解决 "怎么回收" 的问题;
- 分代策略解决 "按对象生命周期优化回收" 的问题;
- G1/ZGC 等进阶算法解决 "大内存、低延迟" 的问题。
实际应用中,需根据业务场景(吞吐量 / 延迟优先)、堆内存大小、CPU 核心数选择合适的收集器,同时通过监控 GC 日志(如 GC 停顿时间、频率、碎片率)调优,避免 Full GC 频繁触发。