在程序运行的世界里,内存就像一块不可再生的“土地”,随着代码的执行,对象不断创建、使用与废弃,若废弃对象占用的内存无法及时回收,就会导致内存泄漏、OOM(内存溢出)等致命问题。垃圾回收(Garbage Collection,简称GC)正是守护内存的“清道夫”,而标记-清除、复制、标记-整理则是GC领域中最核心、最经典的三种算法。本文将深入底层,从原理、流程、优缺点到适用场景,全方位拆解这三种算法,帮你看透GC的工作本质。
一、GC的核心目标:明确“垃圾”并高效回收
在拆解算法前,我们首先要明确两个核心问题:什么是“垃圾”?GC的核心诉求是什么?
所谓“垃圾”,是指程序运行过程中不再被任何活跃对象引用的内存块。比如一个局部变量在方法执行结束后,其引用消失,对应的对象就成了无人问津的垃圾。而GC的核心目标,就是在不影响程序正常运行的前提下,精准识别垃圾、高效回收内存、减少内存碎片,同时尽可能降低对程序性能的影响(如减少STW时间,即Stop The World,停止所有用户线程的时间)。
所有GC算法的设计,都围绕这一目标展开,只是在“识别”和“回收”的策略上各有侧重。而识别垃圾的前提,是掌握对象的引用关系——这就需要依赖“根可达性分析”(以栈帧中的局部变量、静态变量、JNI引用等“根对象”为起点,遍历所有可达对象,未被遍历到的即为垃圾),这是所有现代GC算法的共同基础,本文后续的算法拆解均基于此前提。
二、标记-清除算法:最基础的“识别-删除”范式
标记-清除(Mark-Sweep)算法是GC领域的“鼻祖”,也是最基础的算法,后续很多算法都是在它的基础上优化而来。其核心思路分为“标记”和“清除”两个阶段,流程简单直接。
1. 核心流程:两步完成垃圾回收
标记-清除算法的执行过程可分为两个明确的阶段,整个过程会伴随一次STW(不同实现的STW时间不同,但核心逻辑一致):
-
标记阶段:从根对象出发,遍历所有可达对象,给这些“存活对象”打上标记(标记方式多样,比如通过对象头的标志位、建立单独的标记表等)。此时,未被标记的对象就是待回收的垃圾。
-
清除阶段:遍历整个内存区域,识别出所有未被标记的垃圾对象,直接释放它们占用的内存,并将释放后的内存加入“空闲内存链表”,供后续对象创建时使用。
举个通俗的例子:内存就像一个杂乱的房间,标记阶段相当于“找出所有还在使用的物品并贴标签”,清除阶段则是“把没贴标签的废品扔掉,记录下空出来的位置”。
2. 优缺点:简单却致命的“内存碎片”问题
作为最基础的算法,标记-清除的优势和劣势都极为突出:
优点:
-
实现简单:核心逻辑仅需“标记+清除”两步,无需复杂的内存移动操作,开发成本低。
-
内存利用率较高:仅回收垃圾对象,存活对象无需移动,不会浪费额外的内存空间(比如复制算法需要预留空闲区域)。
缺点:
-
产生内存碎片:这是标记-清除最致命的问题。回收后的内存是分散的小块,虽然总空闲内存足够,但当需要创建大对象时,无法找到连续的内存空间,只能触发一次额外的GC,甚至导致OOM。
-
回收效率不稳定:清除阶段需要遍历整个内存区域,无论内存中存活对象多还是少,都要完整扫描一遍。当内存区域较大时,STW时间会显著增加,影响程序响应速度。
3. 适用场景:存活对象少、大对象少的场景
由于内存碎片问题突出,标记-清除算法单独使用的场景较少,多作为其他算法的辅助。比如在早期的JVM(如JDK 1.2之前的Serial GC)中曾有应用,现在更多见于一些对内存连续性要求不高、对象生命周期短的嵌入式系统或简单应用中。
三、复制算法:用“空间换时间”的高效方案
为解决标记-清除算法的内存碎片问题,复制(Copying)算法应运而生。它的核心思路是“将内存分区,通过复制存活对象来实现无碎片回收”,本质上是用牺牲部分内存空间的代价,换取回收效率和内存连续性。
1. 核心流程:分区复制,存活对象“搬家”
复制算法的核心是将内存划分为两个大小相等的区域,通常称为“From区”(使用区)和“To区”(空闲区),执行过程同样分为两步:
-
标记与复制阶段:从根对象出发,遍历From区中的所有可达对象,将这些存活对象完整复制到To区,并且在复制过程中按顺序排列,避免内存碎片。同时,给复制后的对象打上标记(或直接通过位置区分存活状态)。
-
区域切换阶段:当From区中的存活对象全部复制到To区后,将From区和To区的角色互换——原来的From区变为空闲区,原来的To区变为新的使用区。最后,直接清空原来的From区(此时其中的对象已无存活,无需逐个扫描清除)。
形象地说,这就像把房间分成两个区域,每次只在一个区域放东西;当需要清理时,把有用的东西整齐地搬到另一个区域,然后把原来的区域彻底清空——这样新的区域永远是整齐无碎片的。
2. 优缺点:高效但浪费空间
复制算法的设计思路决定了它与标记-清除算法的优缺点形成鲜明对比:
优点:
-
无内存碎片:存活对象被复制到To区时按顺序排列,回收后内存是连续的,大对象创建时无需担心找不到连续空间。
-
回收效率高:清除阶段无需遍历整个内存区域,只需直接清空From区即可;标记阶段也只需处理存活对象,当存活对象较少时,复制操作的开销极小,STW时间短。
缺点:
-
内存利用率低:内存被划分为两个相等的区域,每次只能使用其中一个,相当于浪费了一半的内存空间。即使To区中大部分空间未被使用,也无法利用。
-
复制开销大:当存活对象较多时(比如老年代),需要大量的复制操作,不仅会增加STW时间,还会消耗CPU资源,效率反而会低于其他算法。
3. 适用场景:存活对象少的“年轻代”
复制算法的特性使其非常适合“对象存活率低”的场景,而JVM的年轻代(Young Generation)正是典型代表——年轻代中的对象大多是“朝生夕死”的,存活时间极短,存活率通常低于10%。因此,现代JVM(如HotSpot)的年轻代普遍采用复制算法的变种(如“ eden + from + to ”的三区域模型,将内存划分为1个eden区和2个survivor区,比例通常为8:1:1,大幅提升了内存利用率)。
四、标记-整理算法:兼顾连续与高效的“进阶方案”
标记-清除有碎片,复制算法浪费空间,那么有没有一种算法能兼顾“内存连续”和“高利用率”?标记-整理(Mark-Compact)算法给出了答案。它的核心思路是在标记-清除的基础上增加“整理”阶段,通过移动存活对象来消除内存碎片,同时保留了较高的内存利用率。
1. 核心流程:标记-整理-清除,三步实现完美回收
标记-整理算法可以看作是标记-清除算法的“增强版”,执行过程分为三个阶段,同样需要STW:
-
标记阶段:与标记-清除算法完全一致,从根对象出发,标记所有可达的存活对象。
-
整理阶段:这是标记-整理算法的核心。遍历内存区域,将所有被标记的存活对象按顺序“移动”到内存的一端,使存活对象集中在一起,形成连续的内存块。
-
清除阶段:直接清空存活对象末尾以外的所有内存区域,释放的内存是连续的,无需建立空闲链表,后续对象可直接在该区域创建。
打个比方,这就像整理房间时,先把有用的东西都搬到房间的一侧并摆整齐,然后把另一侧的废品全部清理掉——既保留了所有有用的东西,又让房间变得整齐无杂物。
2. 优缺点:平衡但有移动开销
标记-整理算法试图在标记-清除和复制算法之间找到平衡,其优缺点也更为均衡:
优点:
-
无内存碎片:通过整理阶段的移动操作,存活对象集中排列,回收后的内存是连续的,满足大对象创建需求。
-
内存利用率高:无需划分额外的空闲区域,所有内存都可用于对象创建,相比复制算法大幅提升了内存利用率。
缺点:
-
移动对象开销大:整理阶段需要移动所有存活对象,不仅要复制对象本身,还要更新所有引用该对象的指针(如其他对象对它的引用、根对象的引用等),这会增加STW时间和CPU开销,尤其当存活对象较多时,开销更为明显。
-
实现复杂:相比前两种算法,标记-整理需要处理对象移动、引用更新等复杂逻辑,开发和优化难度更高。
3. 适用场景:存活对象多的“老年代”
标记-整理算法的特性使其适合“对象存活率高”的场景,而JVM的老年代(Old Generation)正是如此——老年代中的对象大多是从年轻代晋升而来的“长寿对象”,存活率极高(通常超过90%)。此时,移动存活对象的一次性开销,远小于频繁处理内存碎片的代价。因此,现代JVM的老年代常采用标记-整理算法(如Serial Old GC、Parallel Old GC),或标记-清除与标记-整理的结合算法(如CMS GC的并发清除后,通过Serial Old GC进行整理)。
五、三大算法对比与总结:没有最优,只有最适合
以上三种算法各有优劣,没有绝对的“最优解”,实际的GC实现往往是根据内存区域的特性(如对象存活率、对象大小)选择合适的算法,或组合使用多种算法(即“分代回收”思想)。为了更清晰地对比,我们将三种算法的核心特性整理如下:
| 算法类型 | 核心优势 | 核心劣势 | 适用场景 |
|---|---|---|---|
| 标记-清除 | 实现简单、内存利用率较高 | 产生内存碎片、回收效率不稳定 | 存活对象少、大对象少的简单场景 |
| 复制 | 无内存碎片、回收效率高 | 内存利用率低、存活对象多时有复制开销 | 对象存活率低的年轻代 |
| 标记-整理 | 无内存碎片、内存利用率高 | 移动对象开销大、实现复杂 | 对象存活率高的老年代 |
从这些算法的演进中,我们可以看到GC技术的核心发展逻辑:根据场景优化资源分配,在“时间”“空间”“复杂度”之间寻找平衡。现代高级GC(如G1、ZGC、Shenandoah)更是在此基础上,通过区域化内存管理、并发标记、增量整理等技术,进一步降低STW时间,提升回收效率,但它们的核心思想依然源于这三种经典算法。
理解这三种算法的底层机制,不仅能帮助我们更好地排查内存问题(如OOM时判断是否与内存碎片相关),还能为选择合适的GC策略(如年轻代与老年代的算法搭配)提供理论支撑——这正是深入GC底层的价值所在。

868

被折叠的 条评论
为什么被折叠?



