【垃圾回收基石】图解垃圾回收算法:标记-清除、复制、标记-整理、分代收集
想象一下,你住在一个神奇的房间里:你可以随时变出各种物品(创建对象),但从不需要亲自打扫卫生。不过,这个房间有个神秘的清洁工(Garbage
Collector),他会在你不注意的时候,悄悄清理掉你不再需要的物品。今天,就让我们揭开这位清洁工的工作手册,看看他清理房间的几种核心策略。
一、为什么需要垃圾回收?
在开始之前,让我们明确一个基本概念:Java中的对象绝大部分都创建在堆内存上。但内存是有限的,如果只创建不清理,再大的内存也会耗尽。
java
public class MemoryLeakExample {
public static void main(String[] args) {
// 持续创建对象,但不释放引用
List list = new ArrayList<>();
while (true) {
list.add(new Object()); // 最终会导致OutOfMemoryError
}
}
}
垃圾回收(Garbage Collection, GC)就是自动管理堆内存,回收已经"死亡"的对象所占用的空间。那么,GC如何判断对象是否"死亡"呢?
二、垃圾识别的基石:可达性分析算法
GC并不是通过"猜"来找出垃圾的,而是通过一种称为可达性分析(Reachability Analysis) 的算法:
-
从一组称为"GC Roots"的对象作为起点
-
从这些 roots 开始向下搜索,走过的路径称为"引用链"
-
如果一个对象与 GC Roots 之间没有任何引用链相连,则证明此对象不可用
GC Roots 包括:
-
虚拟机栈中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI引用的对象
三、四大垃圾收集算法详解
了解了如何识别垃圾后,我们来看看清洁工实际清理房间的四种策略。
1. 标记-清除算法(Mark-Sweep)
生活类比:你在房间里贴便签标记要扔的东西,然后一次性清理掉。
工作原理:
-
标记阶段:首先遍历所有对象,标记出所有需要回收的对象
-
清除阶段:统一回收所有被标记的对象
可视化过程:
初始状态: [A(活), B(垃圾), C(活), D(垃圾), E(活), F(垃圾)]
标记阶段: [A(活), B(标记), C(活), D(标记), E(活), F(标记)]
↑ GC遍历并标记垃圾对象
清除阶段: [A(活), 空闲, C(活), 空闲, E(活), 空闲]
↑ 清除被标记的对象,留下内存碎片
优点:
-
算法简单,实现容易
-
与复制算法相比,不需要预留一半内存
缺点:
-
效率问题:标记和清除两个过程的效率都不高
-
空间问题:会产生大量不连续的内存碎片,导致以后分配大对象时失败,从而提前触发另一次GC
2. 复制算法(Copying)
生活类比:你把房间分成两半,只使用其中一半。当需要打扫时,把所有需要保留的东西搬到另一半房间,然后彻底清空当前这半房间。
工作原理:
-
将可用内存按容量划分为大小相等的两块
-
每次只使用其中的一块
-
当这一块的内存用完了,就将还存活着的对象复制到另外一块上面
-
然后再把已使用过的内存空间一次清理掉
可视化过程:
内存分为From区和To区:
From区: [A(活), B(垃圾), C(活), D(垃圾)]
To区: [空闲, 空闲, 空闲, 空闲]
复制存活对象到To区:
From区: [A(活), B(垃圾), C(活), D(垃圾)]
To区: [A(活), C(活), 空闲, 空闲]
清空From区:
From区: [空闲, 空闲, 空闲, 空闲]
To区: [A(活), C(活), 空闲, 空闲]
优点:
-
实现简单,运行高效
-
没有内存碎片的问题
缺点:
-
内存缩小为原来的一半,浪费太多空间
-
在对象存活率较高时,复制操作效率会变低
应用场景:商业虚拟机中,复制算法主要用于新生代的垃圾回收,因为新生代中98%的对象都是"朝生夕死"的。
3. 标记-整理算法(Mark-Compact)
生活类比:你不仅标记要扔的东西,还会把保留的东西整齐地推到一边,让空闲空间连续。
工作原理:
-
标记阶段:与"标记-清除"算法一样,首先标记出所有需要回收的对象
-
整理阶段:让所有存活的对象都向一端移动
-
清理阶段:直接清理掉端边界以外的内存
可视化过程:
初始状态: [A(活), B(垃圾), C(活), D(垃圾), E(活), F(垃圾)]
标记阶段: [A(活), B(标记), C(活), D(标记), E(活), F(标记)]
整理阶段: [A(活), C(活), E(活), 空闲, 空闲, 空闲]
↑ 存活对象向一端移动,保持紧凑排列
清理阶段: [A(活), C(活), E(活), 空闲, 空闲, 空闲]
↑ 清理后得到连续的空闲空间
优点:
-
避免了内存碎片问题
-
不需要浪费一半内存空间
缺点:
-
整理阶段涉及大量对象移动,效率较低
-
需要暂停用户程序(Stop The World)
应用场景:主要用于老年代的垃圾回收,因为老年代中对象存活率高,不适合复制算法。
4. 分代收集算法(Generational Collection)
生活类比:你把房间分成两个区域:常用区(放经常更换的小物品)和储藏区(放长期保存的大件物品)。对这两个区域采用不同的清洁策略。
工作原理:
现代商业虚拟机都采用分代收集算法,根据对象存活周期的不同将内存划分为几块:
新生代(Young Generation):存放生命周期短的对象
-
使用复制算法(因为存活对象少,复制成本低)
-
分为Eden空间、From Survivor空间、To Survivor空间(比例通常是8:1:1)
老年代(Tenured Generation):存放生命周期长的对象
-
使用标记-整理或标记-清除算法
-
永久代/元空间(PermGen/Metaspace):存放类信息、常量等(JDK8以后是元空间)
对象晋升流程:
1.新对象首先分配在Eden区
2.当Eden区满时,触发Minor GC,存活对象被复制到Survivor区
3.对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁
4.当年龄达到一定阈值(默认15),就会被晋升到老年代
5.当老年代空间不足时,触发Full GC
四、算法对比与总结
算法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
标记-清除 | 实现简单,不浪费内存 | 效率低,产生碎片 | 一般不单独使用 |
复制 | 效率高,无碎片 | 浪费一半内存 | 新生代(对象存活率低) |
标记-整理 | 无碎片,不浪费内存 | 效率较低 | 老年代(对象存活率高) |
分代收集 | 综合优势,实际应用 | 实现复杂 | 现代商业虚拟机 |
五、现实世界中的GC
实际上,现代的垃圾收集器(如Serial、Parallel、CMS、G1、ZGC等)都是基于这些基础算法的组合和优化。例如:
-
G1收集器:将堆划分为多个Region,采用复制算法进行局部收集
-
ZGC收集器:使用读屏障和颜色指针等技术,极大减少了STW时间
-
理解这些基础算法,是理解复杂垃圾收集器工作原理的关键。
下一篇预告
现在你已经了解了GC如何工作的基本原理,但如何监控GC活动?如何分析GC日志?如何根据实际情况选择合适的垃圾收集器?
下一篇【GC日志分析】,我们将带你实战解析GC日志,让你真正具备诊断和调优GC问题的能力。