在程序运行过程中,内存如同一块有限且不可再生的“资源”,随着代码不断执行,对象被频繁创建、使用并最终废弃。如果这些已废弃的对象所占用的空间无法及时释放,就可能引发内存泄漏,甚至导致OOM(内存溢出)等严重问题。垃圾回收(Garbage Collection,简称GC)正是负责清理这类无效内存的机制,而其中最经典、最基础的三种算法是:标记-清除、复制、以及标记-整理。本文将深入剖析这三大核心算法的工作原理、执行流程、优缺点及其适用场景,帮助你全面理解GC的本质运作逻辑。
1. 垃圾回收的核心目标:识别“垃圾”并高效释放内存
在分析具体算法之前,必须先明确两个关键问题:什么是“垃圾”?GC的主要任务又是什么?
所谓“垃圾”,指的是那些在程序运行中不再被任何活跃引用指向的对象所占据的内存空间。例如,一个方法执行完毕后,其内部的局部变量引用消失,其所关联的对象便成为无人引用的“垃圾”。GC的核心职责就是在不影响程序正常运行的前提下,实现以下三点:
- 准确识别出哪些对象是垃圾
- 高效回收其所占内存
- 尽量减少内存碎片,并降低对应用性能的影响(如缩短STW时间,即Stop-The-World——暂停所有用户线程的时间)
所有GC算法的设计都围绕这一目标展开,区别仅在于“如何识别”和“如何回收”的策略不同。而判断对象是否存活的基础,是基于“根可达性分析”:以栈帧中的局部变量表、静态变量、JNI引用等作为“根对象”,从这些根出发遍历所有可达对象,未被访问到的对象即为可回收的垃圾。这是现代GC算法的共同前提,后文讨论的所有算法均以此为基础。
2. 标记-清除算法:最原始但直观的回收方式
标记-清除(Mark-Sweep)算法是GC发展史上的起点,也是后续多种算法演进的基石。它的设计思想极为直接,分为“标记”与“清除”两个阶段,整体流程简洁明了。
2.1 执行流程:两步走完成内存清理
整个过程通常伴随着一次全局暂停(STW),尽管不同实现的停顿时长有所不同,但基本逻辑一致:
标记阶段:从根对象开始,递归遍历所有可达对象,并为这些“存活对象”打上标记(可通过对象头标志位或独立标记表实现)。此时,未被标记的对象即为待回收的垃圾。
清除阶段:扫描整个堆内存区域,识别出所有未被标记的对象,将其占用的内存空间释放,并将这些空闲块加入“空闲链表”,供后续对象分配使用。
举个生活化的比喻:内存就像一间堆满物品的房间,标记阶段相当于给所有正在使用的物品贴上标签;清除阶段则是把没有标签的杂物全部清走,并记录下腾出的位置。
2.2 优势与缺陷:简单易行却带来碎片化问题
作为最基础的GC算法,标记-清除的优点和缺点都非常明显:
优点:
- 实现简单:仅需“标记 + 清除”两个步骤,无需移动对象,开发难度低。
- 内存利用率高:只释放垃圾对象空间,存活对象保留在原地,不额外消耗内存(不像复制算法需要预留一半空间)。
缺点:
- 产生内存碎片:这是该算法最大的弊端。回收后的内存由多个离散的小块组成,虽然总空闲容量足够,但若要分配大对象,可能因找不到连续空间而触发额外GC,甚至引发OOM。
- 回收效率不稳定:清除阶段需遍历整个内存区域,无论存活对象多少都要完整扫描一遍。当堆空间较大时,会导致较长的STW时间,影响系统响应速度。
2.3 应用场景:适用于特定轻量级环境
由于内存碎片问题突出,标记-清除算法很少单独用于主流JVM的新生代或老年代回收,更多出现在一些特殊环境中。例如早期JVM版本(如JDK 1.2之前的Serial GC)曾采用此算法,目前多见于对内存连续性要求不高、对象生命周期短的嵌入式系统或简单应用程序中。
3. 复制算法:以空间换取时间的高效解决方案
为了克服标记-清除带来的内存碎片问题,复制(Copying)算法被提出。其核心理念是“划分区域、复制存活对象”,通过牺牲一部分内存空间来换取更高的回收效率和更整洁的内存布局。
3.1 工作机制:分区管理与对象迁移
该算法将可用内存划分为两个大小相等的区域,分别称为“From区”(当前使用区)和“To区”(备用空闲区)。回收过程主要包括两个阶段:
标记与复制阶段:从根对象出发,遍历From区内的所有可达对象,将它们完整复制到To区。复制过程中按顺序排列,确保新区域无碎片。同时更新引用关系,使程序能正确访问新位置的对象。
区域交换阶段:当From区中所有存活对象都被复制完成后,将From区与To区角色互换——原From区变为新的空闲区,原To区成为新的使用区。最后,直接清空原From区(其中已无存活对象,无需逐个处理)。
可以类比为整理房间的过程:将房间分为左右两半,平时只在一侧存放物品;清理时,把有用的东西整齐搬到另一侧,然后彻底清空原来那一边。这样每次使用的新区域都是连续且规整的。
3.2 利弊分析:高效但存在空间浪费
复制算法在提升性能方面表现优异,但也付出了代价:
优点:
- 无内存碎片:复制过程天然保证了目标区域的连续性,避免了碎片问题。
- 回收效率高:只需处理From区中的存活对象,无需扫描整个堆,显著缩短STW时间。
- 适合频繁回收:特别适用于对象朝生暮死的场景,如Java虚拟机的新生代GC。
缺点:
- 空间利用率低:必须预留一半内存作为To区,实际可用空间只有总量的一半,造成明显的空间浪费。
- 不适合大对象或高存活率场景:当From区中大部分对象都存活时,复制开销巨大,且To区可能不足以容纳全部数据。
3.3 实际应用:常见于新生代回收
正因为复制算法在处理短命对象时效率极高,它被广泛应用于现代JVM的新生代垃圾收集器中,如Serial New、ParNew等。实际实现中也常通过“幸存区细分”(如Eden+S0+S1)优化空间利用,而非简单的二分法。
复制算法的设计理念与标记-清除算法在优缺点上呈现出显著的互补性:
优点
- 无内存碎片:存活对象被复制到To区时会按顺序排列,回收完成后内存空间保持连续,使得大对象能够顺利分配,无需担忧无法找到足够大的连续空间。
- 回收效率高:清除阶段只需清空From区,无需遍历整个堆;标记过程也仅需处理存活对象。当存活对象较少时,复制开销极小,STW(Stop-The-World)时间短,响应迅速。
缺点
- 内存利用率低:内存被均分为两个区域,每次只能使用其中一半,另一半作为To区备用,即使未完全利用也无法共享。相当于浪费了50%的空间。
- 复制开销大:若存活对象数量较多(如老年代场景),则需要频繁进行对象复制,不仅延长STW时间,还消耗大量CPU资源,整体性能可能不如其他算法。
适用场景:对象存活率低的“年轻代”
由于大多数对象在创建后很快变为不可达(即“朝生夕死”),年轻代中存活对象比例通常低于10%,这恰好契合复制算法的优势条件。因此,现代JVM(如HotSpot虚拟机)普遍在年轻代采用其优化变种——“eden + from + to”三区模型。该模型将内存划分为一个Eden区和两个Survivor区,典型比例为8:1:1,通过动态调整使用区域,显著提升了内存的实际利用率。
四、标记-整理算法:兼顾连续性与空间效率的“进阶方案”
面对标记-清除带来的内存碎片问题,以及复制算法对内存空间的浪费,是否存在一种折中方案?标记-整理(Mark-Compact)算法应运而生。它在标记-清除的基础上引入“整理”步骤,通过对存活对象进行位移来消除碎片,同时保留较高的内存利用率。
核心流程:标记 → 整理 → 清除,三步完成垃圾回收
标记-整理可视为标记-清除的增强版本,执行期间同样要求暂停所有应用线程(STW),主要分为三个阶段:
- 标记阶段:从GC Roots出发,追踪并标记所有可达的对象,确定哪些是仍需保留的存活对象,此过程与标记-清除一致。
- 整理阶段:这是该算法的关键所在。系统会遍历内存区域,将所有已标记的存活对象向内存的一端集中移动,形成一段连续的存储块,从而腾出另一侧的大片空闲区域。
- 清除阶段:清理掉存活对象末尾之后的所有空间,释放出一块完整、连续的可用内存。后续新对象可以直接在此区域分配,无需维护复杂的空闲链表结构。
可以类比为整理房间的过程:先把所有有用的物品集中摆放到一侧并整齐排列,再将剩余区域的杂物彻底清除——结果是空间整洁且高效利用。
优缺点分析:追求平衡但付出一定代价
标记-整理算法试图在前两种策略之间取得折衷,其特性表现为更为均衡的表现:
优点
- 无内存碎片:通过对象移动实现内存紧凑化,确保回收后的空间连续,支持大对象直接分配。
- 内存利用率高:不像复制算法那样固定预留一半空间,所有内存均可用于对象分配,空间利用更加充分。
缺点
- 移动对象开销大:整理阶段涉及大量对象的物理迁移,不仅要复制数据本身,还需同步更新所有指向这些对象的引用指针(包括栈、寄存器、全局引用等),导致STW时间增加,尤其在存活对象众多时尤为明显。
- 实现复杂度高:相比前两者,需额外处理对象地址变更、引用重定向等问题,逻辑更复杂,开发与调优难度更大。
适用场景:对象长期存活的“老年代”
老年代中的对象多由年轻代晋升而来,具有较长生命周期,存活率常超过90%。在此背景下,尽管标记-整理存在一次性移动成本,但相比于因频繁碎片化引发的分配失败或额外整理操作,其总体收益更高。因此,主流JVM的老年代垃圾收集器广泛采用此类算法,例如Serial Old GC和Parallel Old GC;部分混合型收集器(如CMS)也会在并发清除后借助Serial Old进行压缩整理以应对碎片积累。
五、三大算法对比总结:没有最好,只有最合适
上述三种基础垃圾回收算法各具特色,不存在绝对最优的选择。实际GC实现往往依据不同内存区域的特点(如对象存活率、大小分布)灵活选用,或组合运用多种机制,体现“分代回收”的核心思想。以下是对三者关键特性的归纳对比:
| 算法类型 |
核心优势 |
核心劣势 |
适用场景 |
| 标记-清除 |
实现简单,内存利用率较高 |
产生内存碎片,回收效率受碎片影响不稳定 |
适用于存活对象少、大对象少的轻量级场景 |
| 复制 |
无内存碎片,回收速度快 |
内存利用率仅为50%,存活对象多时复制成本高 |
适用于对象死亡率高的年轻代 |
| 标记-整理 |
无内存碎片,内存利用率接近100% |
对象移动开销大,实现复杂 |
适用于对象长期存活的老年代 |
从这些算法的发展脉络中可以看出,垃圾回收技术演进的核心逻辑在于:
根据不同应用场景,在“时间开销”、“空间利用率”与“实现复杂度”之间寻找最佳平衡点。
现代高性能GC(如G1、ZGC、Shenandoah)在此基础上进一步创新,采用分区管理、并发标记、增量整理等手段,有效降低STW时长,提升吞吐量与响应速度。然而,它们的根本设计思想依然根植于这三种经典算法的组合与优化。
掌握这三种算法的底层原理,有助于我们更高效地诊断内存相关问题(例如在发生OOM时,判断其是否与内存碎片有关),同时也为合理选择垃圾回收策略(如年轻代和老年代中不同算法的组合使用)提供了坚实的理论依据。这正是深入理解GC底层机制的重要意义所在。