垃圾回收算法
一、JVM 常见的垃圾回收算法
JVM 垃圾回收主要有以下几种核心算法:
- 标记-清除(Mark-Sweep)
- 复制算法(Copying/Scavenge)
- 标记-整理(Mark-Compact)
- 分代收集(Generational Collection)
- 增量收集(Incremental Collection)
- 分区收集(Region-based Collection)
- 并发收集(Concurrent Collection)
其中,前四种是最基础和常见的算法,后面几种是对基础算法的扩展和优化。
二、各类垃圾回收算法详细介绍
1. 标记-清除(Mark-Sweep)
原理:
- 标记阶段:从 GC Roots(如栈、静态变量等)出发,遍历所有可达对象并做标记。
- 清除阶段:遍历整个堆,把未被标记的对象回收,释放内存。
优点:
- 实现简单,适合回收存活对象较少的场景。
缺点:
- 内存碎片:回收后会留下大量不连续的内存碎片,可能导致后续大对象分配失败。
- 停顿时间长:需要遍历整个堆,回收过程会 Stop-The-World。
适用场景:
- 早期 JVM 或老年代回收。
2. 复制算法(Copying/Scavenge)
原理:
- 将内存分为两块等大小的区域(如 Eden 和 Survivor 区)。
- 每次只使用其中一块(From 区),当这块用满时,GC 把存活对象复制到另一块(To 区),然后清空 From 区。
- 两块区域角色互换。
优点:
- 无碎片:复制后内存连续,分配效率高。
- 回收效率高:只需复制存活对象,适合新生代(大部分对象很快死亡)。
缺点:
- 空间浪费:需要两块等大的内存,实际只用一半。
- 不适合老年代:存活对象多,复制成本高。
适用场景:
- 新生代垃圾回收(如 HotSpot 的新生代采用此算法)。
3. 标记-整理(Mark-Compact)
原理:
- 标记阶段:同标记-清除,标记所有存活对象。
- 整理阶段:将所有存活对象向一端移动,保持内存连续,清理无效空间。
优点:
- 无碎片:整理后内存连续,适合老年代。
- 分配大对象更容易。
缺点:
- 移动对象:需要更新所有引用,成本较高。
- 停顿时间长:整理过程会 Stop-The-World。
适用场景:
- 老年代垃圾回收(如 Serial Old、CMS 的 compact 阶段)。
4. 分代收集(Generational Collection)
原理:
- 根据对象生命周期,将堆分为新生代和老年代。
- 新生代用复制算法,老年代用标记-清除或标记-整理算法。
- 大部分对象“朝生夕死”,少部分对象存活时间长。
优点:
- 提升回收效率:针对不同对象生命周期采用不同算法。
- 减少全堆回收频率。
缺点:
- 需要维护分代和对象晋升逻辑。
适用场景:
- 几乎所有现代 JVM 都采用分代收集。
5. 增量收集(Incremental Collection)
原理:
- 将 GC 过程分为多个小步骤,穿插在应用执行过程中,减少单次停顿时间。
优点:
- 降低单次 GC 停顿,提升应用响应性。
缺点:
- 总体 GC 时间可能增加,实现复杂。
适用场景:
- 对响应时间要求极高的系统。
6. 分区收集(Region-based Collection)
原理:
- 将堆划分为多个小的区域(Region),每次只回收部分区域。
- 代表性算法:G1 GC。
优点:
- 可预测停顿时间,提升大堆内存的回收效率。
- 支持并发和增量回收。
缺点:
- 实现复杂,对硬件和参数要求高。
适用场景:
- 大内存、低停顿需求的服务端应用(如 G1、ZGC、Shenandoah)。
7. 并发收集(Concurrent Collection)
原理:
- GC 线程和应用线程并发执行,减少 Stop-The-World 停顿时间。
- 代表性算法:CMS、G1、ZGC、Shenandoah。
优点:
- 极低停顿,提升应用可用性。
缺点:
- 实现复杂,可能带来额外的 CPU 消耗和“浮动垃圾”(concurrent mark期间新产生的垃圾)。
适用场景:
- 对延迟极敏感的应用。
三、算法与垃圾回收器的关系
- 垃圾回收器(如 Serial、Parallel、CMS、G1、ZGC)通常会结合多种算法实现。
- 例如:G1 GC 结合了分区收集、并发收集、复制和标记-整理等算法。
四、总结
算法名称 | 主要特点 | 适用场景 |
---|---|---|
标记-清除 | 简单,易碎片 | 老年代 |
复制算法 | 无碎片,空间浪费 | 新生代 |
标记-整理 | 无碎片,移动对象 | 老年代 |
分代收集 | 针对生命周期优化 | 现代 JVM 默认 |
增量收集 | 降低单次停顿 | 响应敏感系统 |
分区收集 | 可预测停顿,分区回收 | 大堆、服务端 |
并发收集 | 低停顿,复杂 | 低延迟场景 |
增量收集算法原理
增量收集(Incremental Collection)是一种垃圾回收算法,目的是将一次完整的垃圾回收过程拆分为多个小步骤(小片段) ,这些小步骤穿插在应用程序的正常执行过程中,从而减少每次 GC 停顿(Stop-The-World)的时间,提升应用的响应性。
增量收集的原理
-
分阶段执行
- 将传统的“标记-清除”或“标记-整理”等 GC 过程,拆分为许多小的子任务。
- 每次只执行一部分 GC 工作,然后让应用线程继续运行。
-
交替进行
- GC 线程和应用线程交替执行,GC 做一点,应用跑一会儿,再 GC 一点,如此循环,直到整个回收过程完成。
-
写屏障(Write Barrier)
- 为了保证在应用线程运行时,GC 的标记和清理信息不会被破坏,JVM 会用“写屏障”技术追踪对象的变化,确保垃圾回收的正确性。
增量收集的优缺点
- 优点:大大降低了单次 GC 停顿时间,提升了系统的实时性和响应速度。
- 缺点:GC 总体耗时可能增加,实现复杂,对 CPU 有一定额外消耗。
应用场景
- 对响应时间要求极高的系统(如交互式应用、实时系统)。
- 代表性实现:早期 HotSpot JVM 的
-Xincgc
(Incremental GC,已废弃),现代并发/低停顿 GC(如 G1、ZGC、Shenandoah)也借鉴了增量收集的思想。
总结:
增量收集就是把一次完整的 GC 拆成很多小步,和应用交替执行,从而减少每次 GC 的停顿时间。
GC 停顿
GC 停顿(Stop-The-World,简称 STW)是指在垃圾回收(GC)过程中,JVM 会暂停所有应用线程的执行,只允许 GC 线程工作,直到本次垃圾回收操作完成。
为什么会有 GC 停顿?
-
对象引用关系复杂
- GC 需要遍历和分析所有对象的引用关系,判断哪些对象还活着,哪些可以回收。
- 如果应用线程在 GC 过程中继续运行,可能会不断修改对象引用,导致 GC 得到的对象状态不一致,影响回收的正确性。
-
保证内存回收的安全性和一致性
- 停止应用线程,可以确保 GC 期间对象引用不会被修改,避免“悬挂引用”、“误回收”等问题。
-
简化实现
- 让所有应用线程都暂停,GC 只需关注内存回收本身,算法实现更简单高效。
为什么需要 GC 停顿?
- 数据一致性:只有在所有应用线程都暂停时,GC 才能准确地分析和回收内存,保证不会误删正在使用的对象。
- 避免并发冲突:防止应用线程和 GC 线程同时修改对象,导致内存错误。
- 提升回收效率:集中资源进行回收,减少复杂的并发控制。
总结:
GC 停顿是为了保证垃圾回收期间对象状态的一致性和内存回收的安全性。虽然停顿会影响应用响应,但这是实现高效、正确垃圾回收的必要手段。现代 JVM 不断优化,努力缩短 GC 停顿时间,提升系统性能。
写屏障(Write Barrier)
在增量收集(Incremental GC)或并发/并行 GC中,应用线程和 GC 线程是交替甚至并发运行的,这就带来了“对象引用可能被修改”的一致性问题。
为什么不会破坏对象引用?
- **写屏障(Write Barrier)**就是为了解决这个问题而设计的。
- 它是一种 JVM 层面的机制,用来监控和记录应用线程对对象引用的修改,确保 GC 能正确追踪对象的变化。
写屏障(Write Barrier)原理
-
什么是写屏障?
- 写屏障是一段插入在“对象引用赋值操作”前后的特殊代码。
- 每当应用线程修改对象引用(如
a.field = b
),JVM 都会先执行写屏障逻辑。
-
写屏障的作用
- 记录哪些对象的引用被修改了(通常放入一个“脏卡表”或“记忆集”)。
- 保证 GC 能及时感知到对象引用的变化,不会漏标、误回收。
-
常见写屏障类型
- 前写屏障(Pre-Write Barrier) :在引用被修改前执行,记录旧引用。
- 后写屏障(Post-Write Barrier) :在引用被修改后执行,记录新引用。
- 不同 GC 算法采用不同类型的写屏障。
-
举例说明
- 比如在并发标记阶段,应用线程把对象 A 的字段从指向 B 改为指向 C,写屏障会把 B 或 C 标记到“需要重新扫描”的集合里,GC 线程稍后会补充标记,保证不会漏掉活对象。
总结
- 增量/并发 GC 通过写屏障机制,实时追踪引用变化,保证垃圾回收的正确性和安全性。
- 写屏障是现代 JVM 实现高效、低停顿 GC 的核心技术之一。
并发收集原理
JVM 并发收集(Concurrent GC)的原理,是让垃圾回收线程和应用线程(即用户线程)同时并发执行,以减少或避免长时间的 Stop-The-World(STW)停顿,从而提升应用的响应性和可用性。
并发收集的核心原理
-
并发标记
- 在标记阶段,GC 线程和应用线程同时运行,GC 线程遍历对象图,标记所有可达对象。
- 应用线程可能会修改对象引用,GC 通过“写屏障(Write Barrier)”等机制追踪这些变化,保证标记的准确性。
-
并发清理/回收
- 标记完成后,GC 线程可以在应用线程继续运行的同时,清理不可达对象或回收内存。
-
短暂的 Stop-The-World
- 某些阶段(如初始标记、最终标记、整理内存等)仍需短暂暂停所有应用线程,但大部分回收过程是并发进行的。
关键技术
- 写屏障(Write Barrier) :监控应用线程对对象引用的修改,确保 GC 不会漏标或误回收对象。
- 记忆集(Remembered Set)/卡表(Card Table) :记录哪些区域或对象被修改,便于并发追踪。
- 增量更新/快照-写时(Snapshot-At-The-Beginning, SATB) :不同 GC 算法采用不同策略保证并发标记的正确性。
代表性并发 GC
- CMS(Concurrent Mark Sweep)
- G1 GC
- ZGC、Shenandoah(JDK 11+,更先进的低停顿并发 GC)
总结
JVM 并发收集的本质是让 GC 线程和应用线程并发工作,通过写屏障等机制保证回收的正确性,大幅降低 GC 停顿时间,提升系统的实时性和吞吐量。