JVM中判断对象是否存活的方法
Java中几乎所有的对象实例都存放在堆中,在垃圾收集器对堆内存进行回收前,第一件事情就是要确定哪些对象还“存活”,哪些对象已经“死去”(即不可能再通过任何途径被使用)。
引用计数算法
首先需要声明,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存。
什么是引用计数算法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。那为什么主流的Java虚拟机里面都没有选用这种算法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。在下面的代码中,我们在testGC()中new两个ReferenceCountingGC对象objA和objB,然后令objA.instance=objB,objB.instance=objA,最后令objA和objB都为null。此时,这两个对象实际上已经不可能再被访问,但由于它们互相引用着对方,导致它们的引用计数器值都不为0,于是使用引用计数算法无法通知GC收集器回收它们。
public 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; // 假设在这行发生GC,objA和objB是否能被回收? System.gc(); } public static void main(String[] args) { testGC(); } }
现在,我们在main方法中调用testGC(),然后打印GC日志(在Myeclipse中打印GC日志的方法)。得到结果为:
0.161: [GC 4761K->568K(124416K), 0.0022505 secs]
0.163: [Full GC 568K->471K(124416K), 0.0201927 secs]
由“4761K->568K”我们可以得知,虚拟机并没有因为这两个对象相互引用就不回收它们,这也从侧面说明了hotspot虚拟机并不是通过引用计数算法来判断对象是否存活的。
那么,在主流的商用程序语言的主流实现中,是通过什么方法来判断对象是否存活的呢?答案是下面将要介绍的可达性分析算法。
可达性分析算法
可达性分析算法的基本思路是:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点考试向下探索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如下图中,对象object5、object6、object7虽然互有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
1. 虚拟机栈(栈帧中的本地变量表)中引用的对象;
2. 方法区中类静态属性引用的对象;
3. 方法区中常量引用的对象;
4. 本地方法栈中JNI(即一般说的Native方法)引用的对象。
实例分析
参考:https://www.zhihu.com/question/21539353/answer/95667088垃圾回收机制中,引用计数法是如何维护所有对象引用的? - Gityuan的回答 - 知乎 https://www.zhihu.com/question/21539353/answer/95667088
以一段代码为例,从内存角度解释以上两种算法。
public class GcDemo { public static void main(String[] args) { //分为6个步骤 GcObject obj1 = new GcObject(); //Step 1 GcObject obj2 = new GcObject(); //Step 2 obj1.instance = obj2; //Step 3 obj2.instance = obj1; //Step 4 obj1 = null; //Step 5 obj2 = null; //Step 6 } } class GcObject{ public Object instance = null; }
情况(一):引用计数算法
如果采用的是引用计数算法:
再回到前面代码GcDemo的main方法共分为6个步骤:
- Step1:GcObject实例1的引用计数加1,实例1的引用计数=1;
- Step2:GcObject实例2的引用计数加1,实例2的引用计数=1;
- Step3:GcObject实例2的引用计数再加1,实例2的引用计数=2;
- Step4:GcObject实例1的引用计数再加1,实例1的引用计数=2;
执行到Step 4,则GcObject实例1和实例2的引用计数都等于2。
接下来继续结果图:
- Step5:栈帧中obj1不再指向Java堆,GcObject实例1的引用计数减1,结果为1;
- Step6:栈帧中obj2不再指向Java堆,GcObject实例2的引用计数减1,结果为1。
到此,发现GcObject实例1和实例2的计数引用都不为0,那么如果采用的引用计数算法的话,那么这两个实例所占的内存将得不到释放,这便产生了内存泄露。
情况(二):可达性算法
这是目前主流的虚拟机都是采用GC Roots Tracing算法,比如Sun的Hotspot虚拟机便是采用该算法。 该算法的核心算法是从GC Roots对象作为起始点,利用数学中图论知识,图中可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存。这里涉及两个概念,一是GC Roots,一是可达性。
那么可以作为GC Roots的对象(见下图):
- 虚拟机栈的栈帧的局部变量表所引用的对象;
- 本地方法栈的JNI所引用的对象;
- 方法区的静态变量和常量所引用的对象;
关于可达性的对象,便是能与GC Roots构成连通图的对象,如下图:
从上图,reference1、reference2、reference3都是GC Roots,可以看出:
- reference1-> 对象实例1;
- reference2-> 对象实例2;
- reference3-> 对象实例4;
- reference3-> 对象实例4 -> 对象实例6;
可以得出对象实例1、2、4、6都具有GC Roots可达性,也就是存活对象,不能被GC回收的对象。
而对于对象实例3、5直接虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,这就是GC需要回收的垃圾对象。
到这里,相信大家应该能彻底明白引用计数算法和可达性算法的区别吧。
再回过头来看看最前面的实例,GcObject实例1和实例2虽然从引用计数虽然都不为0,但从可达性算法来看,都是GC Roots不可达的对象。
总之,对于对象之间循环引用的情况,引用计数算法,则GC无法回收这两个对象,而可达性算法则可以正确回收。