在 Java 中,垃圾回收(Garbage Collection, GC)是一个至关重要的功能,它能够自动管理内存,回收不再使用的对象,从而防止内存泄漏。然而,在垃圾回收的实现上,JVM 并未采用引用计数算法(Reference Counting),而是使用了可达性分析算法(Reachability Analysis)。
那么,为什么 JVM 选择可达性分析,而不是引用计数?这篇文章将深入探讨引用计数的原理、局限性,以及 JVM 采用可达性分析的原因。
1. 什么是引用计数算法?
1.1 引用计数算法的基本原理
引用计数是一种简单且高效的垃圾回收策略,它的核心思想是:
-
每个对象维护一个引用计数器,记录有多少个变量或其他对象引用它。
-
当有新的引用指向该对象时,计数器 +1。
-
当一个引用失效(比如变量赋值为
null
)时,计数器 -1。 -
当计数器降为 0 时,说明该对象不再被任何变量或对象引用,可以被垃圾回收。
1.2 引用计数的示例
class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
// 占用内存,以便观察 GC 发生情况
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
按照引用计数算法的逻辑:
-
objA
和objB
互相引用,导致它们的引用计数始终不为 0。 -
即使
objA = null; objB = null;
,它们仍然引用彼此,引用计数不会降为 0。 -
结果:垃圾回收器无法回收这两个对象,导致内存泄漏。
2. JVM 为什么不采用引用计数算法?
2.1 关键问题:循环引用问题
正如上面的例子所示,引用计数算法无法处理循环引用(Circular Reference)。
在 Java 这种广泛使用对象引用的语言中,循环引用的情况很常见。如果采用引用计数算法,会导致大量的对象无法被正确回收,从而引发内存泄漏,严重影响应用程序的稳定性。
解决方案?
-
可达性分析算法(Reachability Analysis),也称作可达性遍历。
3. 可达性分析算法(JVM 的 GC 方案)
3.1 可达性分析的基本原理
JVM 采用可达性分析算法来判断对象是否存活,其核心思想是:
-
以一组**根对象(GC Roots)**作为起点。
-
从 GC Roots 开始遍历对象的引用链(Reference Chain)。
-
可达的对象被认为仍然存活,不可达的对象则会被回收。
GC Roots 包含哪些对象?
-
栈上的局部变量(方法内的变量)。
-
静态变量(属于类的静态字段)。
-
运行时常量池中的引用(比如字符串常量池
String Table
)。 -
本地方法栈 JNI 引用(Native 代码引用的对象)。
-
JVM 内部特殊对象(如类加载器、异常对象等)。
3.2 可达性分析示例
4. JVM 的四种引用类型
为了更好地管理对象,Java 1.2 之后引入了四种引用类型:
4.1 强引用(Strong Reference)
Object obj = new Object();
-
只要强引用存在,GC 永远不会回收该对象。
4.2 软引用(Soft Reference)
SoftReference<Object> softRef = new SoftReference<>(new Object());
-
适用于缓存,内存不足时才会被回收。
4.3 弱引用(Weak Reference)
WeakReference<Object> weakRef = new WeakReference<>(new Object());
-
下一次 GC 就会回收,用于存储敏感数据(如
ThreadLocal
)。 -
ThreadLocal
中的ThreadLocalMap
采用了弱引用,防止内存泄漏。
4.4 虚引用(Phantom Reference)
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());
-
对象被 GC 回收时会进入
ReferenceQueue
,用于监控对象回收情况。 -
常用于管理堆外内存或跟踪对象销毁。
5. 结论
-
JVM 不使用引用计数算法的主要原因是:无法解决循环引用问题。
-
JVM 采用可达性分析算法,通过 GC Roots 遍历对象引用链来判断对象是否存活。
-
JVM 引入四种引用类型,以增强垃圾回收的控制能力,适应不同场景需求。
JVM 的 GC 机制不断优化,比如 G1、ZGC 采用了更先进的垃圾回收策略,使得 Java 的内存管理更高效。