首页 > Java > Java GC之对象已死吗

Java GC之对象已死吗

差不多两年以前曾经写过一篇文章:JAVA 性能调优,其实在那篇文章中只是简单的说了,对象的分布。这篇文章继续对分布于堆中的对象的生命周期进行说明,也就是确定堆中的这些对象哪些还是“活着”的,哪些是已经“死去”(即不可能再被任何途径使用的对象)的。

1. 引用计数算法

有很多人认为判断对象是否活着的算法是这样的:给对象添加一个引用计数器,每当有一个地方引用他的时候,计数器就加1,引用失效的时候,计数器减1,当计数器的数值为0时就是不可能在被引用的对象,此时就就可以认为是已死的对象。引用计数器算法实现简单,效率也很高,是一个不错的算法,但是主流的Java虚拟机并没有采用这种算法来管理内存,其中最主要的原因就是:它很难解决对象之间循环引用的问题。
举一个简单的例子:对象objA和objB都有字段instance,赋值令,除此之外,这两个对象再无任何引用,实际上他们已经不可能在被访问到,但是他们因为相互引用对方,计数器都不可能为0,计数器算法是无法通知GC收集器收集他们的。


package demo;

/**
 * testGC()方法执行后,objA和objB会不会被GC呢?
 * 
 * @author BridgeLi
 * 
 */
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) {
        ReferenceCountingGC.testGC();
    }
}

从这个例子的运行结果来看,虚拟机并没有这两个对象存在相互引用就不收集他们,从而证明了Java虚拟机不是通过引用计数算法来判断对象是否已死的。

2. 可达性分析算法

该算法的基本思路就是通过一系列成为“GC Roots”的对象作为起始点,从这些起点向下搜索,搜索所走过的路径称为引用链(Reference chain),当一个对象到GC Roots没有任何引用链相连时,则此对象就是不可用的。在Java语言中,可作为GC Roots的对象包括下面几种:

①. 虚拟机栈中引用的(栈帧中本地变量表)对象
②. 方法区中类静态属性引用的对象
③. 方法区中常量引用的对象
④. 本地方法栈中JNI引用的对象

可达性分析算法的示意图,如下:

jvmgc

3. 生存还是死亡

其实即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这个时候他们处于“缓刑”阶段,至少要经历两次标记过程:如果对象在进行可达性分析后没有与GC Roots相连接的引用链,那他将会被第一次标记并且进行一次筛选,筛选的条件是此对象有无必要执行finalize()方法,当对象没有覆盖finalize()方法或者finalize()已经被虚拟机执行过,将被视为没有必要执行。如果该对象被判定为有必要执行finalize()方法,那么该对象将会被放置在一个叫做F-Quene的队列之中,稍后虚拟机会自动建立一个低优先级的Finzlizer线程去执行他。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待他运行结束,这么做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将很有可能会导致F-Quene队列中的其他对象永远处于等待,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Quene中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己–只要重新与引用链上的任何一个对象管理上即可,那么第二次标记时他将被移出“即将回收”的集合,如果对象在这个时候还没有逃脱,那他就真的死了


package demo;

/**
 * 此代码演示了两点: 1. 对象可以在被GC时自我拯救。 2. 这种自我拯救机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
 * 
 * @author BridgeLi
 * 
 */
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, I am still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();

        // 对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize()方法优先级很低,所以暂停0.5秒以等待他
        Thread.sleep(500);
        if (null != SAVE_HOOK) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, I am dead");
        }

        // 这段代码和上面的代码相同,但是这次自我拯救却失败了
        SAVE_HOOK = null;
        System.gc();
        // 因为finalize()方法优先级很低,所以暂停0.5秒以等待他
        Thread.sleep(500);
        if (null != SAVE_HOOK) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, I am dead");
        }
    }
}

需要说明的是,大家尽量不要使用这种方法来拯救对象,这只是Java刚诞生时为了是c和c++程序猿更容易接受而做的一个妥协,他的运行代价高,不确定性大,无法保证各个对象的调用顺序。

参考资料:周志明《深入理解Java虚拟机》第二版第三章

分享到:
作 者: BridgeLi,http://www.bridgeli.cn/
原文链接:https://www.bridgeli.cn/archives/330
版权声明:非特殊声明均为本站原创作品,转载时请注明作者和原文链接。
分类: Java 标签: ,
  1. 本文目前尚无任何评论.