简介

垃圾收集器对堆内存进行回收时,首先要判断堆中的对象是否还存活。

对象是“生存”还是“死亡”?这是一个问题。经典的判断算法是引用计数法,但存在一定缺陷。现代虚拟机是通过可达性分析算法判断对象是否存活。

引用计数算法

引用计数法是一个很好理解的算法。在对象中添加一个引用计数器,每当有一个引用指向该对象时,就将计数器值加一;当引用失效时,计数器值减一。任何时刻计数器值为零的对象可认为是“死亡”的。

一切都看似很正常,但实际上存在循环引用的问题。请看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ReferenceCountingGc {
public Object instance;
private byte[] bigSize = new byte[2 * 1024 * 1024];
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();
}
}

分析:testGc()方法被调用时,JVM同步创建一个栈帧压入当前执行线程的虚拟机栈中,随后,虚拟机栈中存在两个引用objAobjB分别指向堆内的两个new ReferenceCountingGc()对象,然后,堆内两个对象的成员变量instance互相指向对方,再然后,将虚拟机栈内的引用指向null,最后触发gc。此时,堆内的这两个对象互相引用着对方,引用计数器的值不为零,但它们已经无法再由程序进行访问了,所以如果采用引用计数算法进行垃圾回收,这两个对象将无法被回收,即会出现内存泄露问题。

可达性分析算法

现代标准虚拟机都是采用可达性分析算法判断对象是否存活。基本思路是通过一系列称为GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连,则称该对象是不可达的,即需要被回收。

Java技术体系里,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的局部变量表)中引用的对象,例如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,即一个Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,例如字符串常量池中的引用。
  • 在本地方法栈中本地Native方法引用的对象。
  • 虚拟机内部的引用,例如基本数据类型对应的Class对象;一些常驻的异常类对象,例如NullPointExceptionOutOfMemoryError等,另外还有系统类加载器类对象。
  • 所有被同步锁(synchronized)持有的对象。
  • 反应虚拟机内部情况的JMXBeanJVMTI中注册的回调和本地代码缓存等。

以上是固定作为GC Roots节点集的对象,但虚拟机还会根据所选垃圾收集器和当前进行垃圾回收的内存区域,可以将一些其它对象“临时性”地加入节点集,共同组成完整的GC Roots集合,确保可达性分析的正确性。

参考

  • 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 - 周志明