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个步骤:

  • Step1GcObject实例1的引用计数加1,实例1的引用计数=1
  • Step2GcObject实例2的引用计数加1,实例2的引用计数=1
  • Step3GcObject实例2的引用计数再加1,实例2的引用计数=2
  • Step4GcObject实例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算法,比如SunHotspot虚拟机便是采用该算法。 该算法的核心算法是从GC Roots对象作为起始点,利用数学中图论知识,图中可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存。这里涉及两个概念,一是GC Roots,一是可达性。

 

那么可以作为GC Roots的对象(见下图):

 

  • 虚拟机栈的栈帧的局部变量表所引用的对象;
  • 本地方法栈的JNI所引用的对象;
  • 方法区的静态变量和常量所引用的对象;

 

关于可达性的对象,便是能与GC Roots构成连通图的对象,如下图:

 

 

 从上图,reference1、reference2、reference3都是GC Roots,可以看出:

  • reference1-> 对象实例1
  • reference2-> 对象实例2
  • reference3-> 对象实例4
  • reference3-> 对象实例4 -> 对象实例6

可以得出对象实例1246都具有GC Roots可达性,也就是存活对象,不能被GC回收的对象。
而对于对象实例35直接虽然连通,但并没有任何一个GC Roots与之相连,这便是GC Roots不可达的对象,这就是GC需要回收的垃圾对象。

到这里,相信大家应该能彻底明白引用计数算法和可达性算法的区别吧

再回过头来看看最前面的实例,GcObject实例1和实例2虽然从引用计数虽然都不为0,但从可达性算法来看,都是GC Roots不可达的对象。

总之,对于对象之间循环引用的情况,引用计数算法,则GC无法回收这两个对象,而可达性算法则可以正确回收。

 

原文地址:https://www.cnblogs.com/thiaoqueen/p/8759374.html