在 Java 编程的世界里,内存管理是至关重要的一环,而垃圾回收机制则像是一位幕后 “清洁工”,默默保障着程序运行时内存空间的整洁与高效利用。其中,可达性分析算法担当着举足轻重的角色,精准判断对象 “生死”,助力 Java 内存实现自动化管理。
一、垃圾回收与内存管理的 “刚需”
二、可达性分析算法 “初印象”
三、核心 “角色”:根节点集合
四、工作 “流程” 大揭秘
五、优势与局限 “权衡”
六、优化 “招式” 应对挑战
除了可达性分析算法,Java中还有哪些垃圾回收算法?
引用计数算法(Reference Counting)
- 原理:
- 该算法给每个对象添加一个引用计数器,每当有一个地方引用这个对象时,计数器就加 1;而当引用失效(比如某个变量不再指向该对象)时,计数器就减 1。当对象的引用计数器的值为 0 时,就表示这个对象已经没有被引用了,可以被当作垃圾回收了。
- 例如,假设有对象 A、B、C,初始时它们的引用计数器都为 0。当创建了一个变量指向 A 时,A 的引用计数器变为 1;如果这个变量又被用来指向 B 了,那么 A 的引用计数器减 1 变为 0,此时 A 就满足被回收的条件了,而 B 的引用计数器变为 1。若后续又有新的变量指向 A,A 的引用计数器又会加 1,重新变为可存活状态。
- 优点:
- 实现简单,易于理解,判断对象是否可回收的逻辑比较直观,不需要像可达性分析那样复杂的追踪过程。
- 实时性较高,对象一旦没有引用了,就可以立马被回收,不需要等待特定的垃圾回收周期。
- 缺点:
- 无法解决循环引用的问题。比如对象 A 引用对象 B,对象 B 又引用对象 A,此时它们的引用计数器都不为 0,但实际上这两个对象可能已经没有外部引用了,按道理应该被回收,可引用计数算法却无法正确处理这种情况。例如在以下代码中:
class ObjA {
ObjB b;
}
class ObjB {
ObjA a;
}
public class Main {
public static void main(String[] args) {
ObjA a = new ObjA();
ObjB b = new ObjB();
a.b = b;
b.a = a;
a = null;
b = null;
// 此时a和b对象存在循环引用,引用计数算法不会回收它们,会造成内存泄漏
}
}
- 额外的空间开销,因为要为每个对象维护一个引用计数器,对于内存资源来说是一种消耗,尤其是在对象数量众多的情况下。
- 应用场景:由于存在循环引用等难以解决的问题,在 Java 的主流垃圾回收器中一般不会单独使用该算法,但在一些其他的编程语言或者特定简单场景下有应用,比如 Python 语言中对部分对象的回收有采用引用计数配合其他机制的方式。
标记 - 清除算法(Mark-Sweep)
- 原理:
- 分为 “标记” 和 “清除” 两个阶段。首先从根节点(如虚拟机栈中的局部变量、方法区中的静态变量等)开始,通过遍历对象引用关系,对所有从根节点可达的对象进行标记,表示它们是存活的。然后,遍历整个堆内存,将没有被标记的对象进行清除,回收它们所占的内存空间。
- 比如有一个堆内存空间存放着多个对象,在标记阶段,顺着根节点出发的引用链,把能访问到的对象都打上标记,之后在清除阶段,把那些没标记的对象所占的内存区域清理掉,让其变为可分配的空闲内存。
- 优点:
- 实现相对简单,不需要复杂的额外数据结构来辅助,只要能标记对象和遍历堆内存就可以实现基本的垃圾回收功能。
- 缺点:
- 效率问题,标记和清除这两个过程的效率都不高,标记过程需要遍历所有对象,清除过程也要再次遍历堆内存,比较耗时,尤其是在内存空间较大、对象数量众多的情况下,会对程序性能产生较大影响。
- 内存碎片化严重,清除后的空闲内存空间是不连续的,后续分配内存时,可能出现虽然总的空闲内存足够,但由于碎片化无法找到合适连续空间来分配较大对象的情况,影响内存分配效率。
- 应用场景:该算法是一种比较基础的垃圾回收算法,现代的一些垃圾回收器虽然不会单纯采用它,但它的思路常被借鉴或者作为更复杂回收算法中的一部分,例如在一些简单的、对性能要求不高且内存分配相对简单的环境中可以使用。
复制算法(Copying)
- 原理:
- 将可用内存划分为大小相等的两块,每次只使用其中一块。当进行垃圾回收时,把存活的对象从正在使用的这块内存复制到另外一块空闲的内存中,然后把正在使用的这块内存整个清空,这样一来,空闲的那块内存就变成了下一次分配对象的内存空间,而原来那块内存就完全空闲下来等待下次回收时再重复这个过程。
- 例如内存被划分为 A、B 两块区域,初始在 A 区分配对象,回收时,把 A 区中存活的对象复制到 B 区,然后 A 区所有空间被清空,下次就从 B 区分配对象,下次回收时再把 B 区存活对象复制回 A 区,循环操作。
- 优点:
- 实现简单,只需要复制存活对象,不需要像标记 - 清除算法那样去处理复杂的空闲内存整合等问题。
- 解决了内存碎片化问题,每次回收后,内存空间都是连续完整的,分配内存时速度更快,只要内存空间足够存放存活对象就行。
- 缺点:
- 内存利用率低,因为始终有一半的内存空间处于空闲状态,对于内存资源来说是一种浪费,特别是在内存资源紧张的情况下,这种浪费比较明显。
- 如果存活对象较多,复制的开销会比较大,在复制过程中需要耗费一定的时间和系统资源来完成对象的复制操作。
- 应用场景:比较适用于对象存活率较低的场景,比如在新生代(Young Generation)中,大部分对象都是朝生夕死的,采用复制算法可以高效地回收内存,减少内存碎片化,像 Java 的 Serial、ParNew 等新生代垃圾回收器就采用了复制算法或者基于其改进的算法。
标记 - 整理算法(Mark-Compact)
- 原理:
- 同样先进行标记阶段,从根节点出发标记所有存活的对象,和标记 - 清除算法的标记阶段类似。但在清除阶段,不是简单地把未标记对象清除,而是将所有存活的对象向一端移动,然后直接清理掉存活对象边界以外的内存空间,使得内存空间在回收后依然是连续的,便于后续对象的分配。
- 比如堆内存中有很多对象分布在不同位置,经过标记后,把存活的对象往内存的一端(比如低地址端)移动,让它们紧凑排列,之后把另一端的空闲空间全部释放掉,形成连续的空闲内存区域。
- 优点:
- 解决了标记 - 清除算法的内存碎片化问题,经过整理后内存空间连续,提高了内存的利用率以及后续内存分配的效率。
- 缺点:
- 移动存活对象的成本较高,尤其是在存活对象较多、内存空间较大的情况下,需要耗费较多的时间和系统资源来完成对象的移动整理工作,对程序的性能会产生一定影响。
- 应用场景:适用于老年代(Old Generation)等对内存空间连续性要求较高、对象存活率相对较高的区域,像 Java 的 Serial Old 等垃圾回收器在老年代回收中会采用这种算法来保障内存的有效利用和合理布局。