JVM垃圾收集器

对象已死?

在堆里面存放着Java中几乎所有的对象实例,垃圾收集器在对堆进行回收前,第一件时间就是要确定哪些对象还 "存活" 着,哪些已经 "死去"(代表即不可能再被任何途径使用的对象)了。

引用计数算法

原理

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

优点

  • 简单直接
  • 判定效率高

    缺点

    占用了额外的内存空间进行计数

  • 需要额外的内存空间来进行计数
  • 无法解决对象之间的相互循环依赖问题

对于互相循环引用,请看以下代码,testGC()方法中有对象objA和objB,对象属性中有instance,赋值 objA.instance = objB及objB.instance = objA,除此之外,这两个对象再无任何引用,实际上两个对象已经不可能再被访问,但是它们因为相互引用着对方,导致它们的引用计数都不为0,引用计数器算法也就无法回收它们。

执行结果

JVM垃圾收集器_第1张图片

从运行结果中可以看清楚内存回收日之中包含 "6717K->608K",意味着虚拟机并没有因为这两个对象相互引用就放弃回收它们,这也从侧面说明了Java虚拟机并不是通过引用计数算法来判断对象是否存活的。

可达性分析算法

原理

可达性分析算法的基本思路就是通过一系列称为 "GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为 "引用链"(Reference Chain),如果某个对象到GC Roots 间没有任何引用链相连,或者用图论到话来说就是从GC Roots到这个对象不可达时,则证明此对象时不可能再被使用的。

如下图所示,对象obj5、obj6、obj7虽然有关联,但是它们到GC Roots是不可达到,因此他们将会判定为可回收的对象。

JVM垃圾收集器_第2张图片

在Java中,固定可作为GC Roots的对象包括以下几种:

  • 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如当前正在运行的方法所用到的参数、局部变量、临时变量等。
  • 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
  • 在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。
  • 在本地方法栈中JNI(通常所说的Native方法)引用的对象。
  • Java虚拟机内部的引用,如基本类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器。
  • 所有被同步锁(synchronized关键字)持有的对象。
  • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

优点

可达性分析可以解决引用计数器所不能解决的循环引用问题

目前最新的几款垃圾回收器无一例外都具备了局部回收的特征,为了避免GC Roots包含过多对象而过度膨胀,它们在实现上也做出了各种优化处理。

再谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活和 "引用"离不开关系。在JDK1.2版之前,Java里面的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称该reference数据时代表某块内存、某个对象的引用。这种定义并没有什么不对,只是现在看来有些过于狭隘了,一个对象在这种定义下只有 "被引用" 或者 "未被引用" 两种状态,对于描述一些 "食之无味,弃之可惜" 的对象就显得无能为力。譬如我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象--很多系统的缓存功能都符合这样的应用场景。

在JDK1.2版之后,Java对引用的对象进行了扩充,将引用分为强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次减弱。

  • 强引用是最传统的 "引用" 的定义,是指在程序代码之中普遍存在的引用赋值,即类似 "Object obj = new Object()" 这种引用关系。无论任何情况下,只要强引用关系还存在,还可达,垃圾收集器就永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后提供了SoftReference类来实现软引用。
  • 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK1.2版本之后提供了WeakReference类来实现若引用。
  • 虚引用也称为 "幽灵引用" 或者 "幻影引用",它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来去的一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK1.2版之后提供了PhantomReference类来实现虚引用。

生存or死亡?

即使在可达性分析算法中被判定为不可达到对象,也不是 "非死不可" 的,这时候它们还在世处于 "缓刑" 阶段,要真正宣告一个对象死亡,最多会经历两次标记过程:如果对象在进行可达分析之后发现没有与GC Toots相连接的引用链,那他将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为 "没有必要执行"。

如果这个对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法。这里所说的 "执行" 是指虚拟机会触发这个方法开始运行,但并不承诺一定会等待它运行结束。这样做的原因是,如果某个对象的finalize()方法执行缓慢,或者更极端地发生了死循环,将很可能导致F-Queue队列中的其他对象永久处于等待,甚至导致整个内存回收子系统的崩溃。finalize()方法是对象逃脱死亡的最后一次机会,稍后收集器将堆F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己————只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将会被移出 "即将回收" 的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。从以下代码中我们可以看到一个对象的finalize()被执行,但是它依然可以存活。

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

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("是的,我还活着!");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize 方法被执行!");

        // 被静态属性引用,可完成自救,在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量可作为GC Roots
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

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

        // 对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,等待它
        TimeUnit.MILLISECONDS.sleep(500);
        if (SAVE_HOOK != null)
            SAVE_HOOK.isAlive();
        else
            System.out.println("我已经死了");


        // 下面的代码与上面完全相同,但是这次却自救失败了(一个对象自救的机会只有一次)
        SAVE_HOOK = null;
        System.gc();
        // 因为Finalizer方法优先级很低,暂停0.5秒,等待它
        TimeUnit.MILLISECONDS.sleep(500);
        if (SAVE_HOOK != null)
            SAVE_HOOK.isAlive();
        else
            System.out.println("我已经死了");

    }
}

运行结果:

JVM垃圾收集器_第3张图片

从以上代码的运行结果可以看到,SAVE_HOOK对象的finalize()方法确实被垃圾收集器触发过,并且在被收集前成功自我拯救了。

另外一个值得注意的地方是,代码中有两段完全一样的代码片段,执行结果却是一次自我拯救成功,一次失败了。这是因为任何一个对象的finalize()方法只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会再被执行,因此第二段代码的自救行动失败了。

finalize()方法是Java刚诞生时为了使传统C、C++程序员更容易接受Java所做出的一项妥协。它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。

你可能感兴趣的:(JVM垃圾收集器)