聊聊Java的引用类型(一)
2019-12-16 | 分类 Java | 标签 Java Reference 垃圾回收

前言

Java在语言层面支持两种类型:原始类型(primitive type)引用类型(reference type)。如果读者学过C语言的话,可以发现Java的引用类型类似于C语言里的指针类型。

Java是一门带有垃圾回收特性的编程语言,当一个对象没有被引用的时候垃圾回收器就可以把这个对象回收掉以释放它占用的内存空间。垃圾回收器判断一个对象有没有被引用的依据就是检查一个对象是否有强引用。

我们平时创建一个对象,比如:Integer intA = new Integer(1);,就是把在堆里创建的对象赋值给了intA这个变量,使得intA这个变量持有这个对象的引用,而这个引用在Java里面被称为强引用。那么既然我们说引用有强引用类型,那是不是还有弱引用的说法呢?

确实,Java除了支持强引用之外还支持弱引用,而且针对引用的强弱程度,弱引用类型还可以细分为:软引用(SoftReference)弱引用(WeakReference)虚引用(PhantomReference)。本文我们就来聊聊Java引入的这几个引用类型。

四种引用

强引用

在Java中普通的引用类型就是强引用,比如当我们new一个新的对象并将这个对象的赋值给一个变量的时候,我们就对这个对象创建了一个强引用。Java是一门自带垃圾回收的语言,垃圾收集器在收集垃圾内存的时候就是通过判断一个对象是否有强引用关联来决定是否需要回收这个对象,如果一个对象没有强引用关联,那么这个对象就会被垃圾回收器标记为垃圾对象,并在未来的GC周期中被回收掉。所以,在Java中强引用是默认的,我们不需要显式创建对象的强引用,而弱引用就恰恰相反,它是一种需要我们显式指定的引用类型,并且和Java的垃圾回收机制息息相关。

下面我们来看下Java四种引用类型中除强引用之外的另外三种弱引用类型。

软引用

首先介绍软引用,Java中的 软引用 也叫 Soft reference,由Java中的SoftReference类表示。软引用和强引用的区别主要体现在:一个对象如果是 软可达(soft reachable) 的,那么这个对象就可以在未来JVM内存不足导致抛出 OOM(OutOfMemory Exception) 前被垃圾收集器回收掉,以释放被这些对象占用的空间。

Java官方文档1soft reachable 的定义如下:

An object is softly reachable if it is not strongly reachable but can be reached by traversing a soft reference.

从定义中可以看出:一个对象如果没有强引用关联只有软引用关联,则这个对象就满足 soft reachable 条件。满足 soft reachable 条件的对象会一直存活到JVM内存不够导致OOM的时候,所以SoftReference类型的一个使用场景就是用来实现本地内存缓存(Local cache)。

public SoftReference(T referent);
public SoftReference(T referent, ReferenceQueue<? super T> q);
public T get();

SoftReference类型提供了两个构造方法和一个get()方法。一个对象如果要被软引用关联,可以将这个对象作为SoftReference构造函数的参数传入,这样构造的SoftReference对象就持有了这个对象的软引用。SoftReference中的第二个构造方法中有一个ReferenceQueue类型的参数,这个参数的作用这里先放放,下面在讲到引用队列ReferenceQueue的时候会专门讲引用队列的作用。当需要通过这个软引用对象获取被引用的对象的时候,可以通过get()方法获取。如果一个软引用关联的对象被垃圾回收了,那么调用get()方法将返回null值。

下面是一个例子:

public class Test {
    public static void main(String ...args) {
        byte[] hugeBlock1 = new byte[4 * 1024 * 1024]; // 4MB
        SoftReference<byte[]> hugeBlockRef1 = new SoftReference<>(hugeBlock1);
        hugeBlock1 = null;

        byte[] hugeBlock2 = new byte[4 * 1024 * 1024]; // 4MB
        SoftReference<byte[]> hugeBlockRef2 = new SoftReference<>(hugeBlock2);
        hugeBlock2 = null;

        byte[] hugeBlock3 = new byte[4 * 1024 * 1024]; // 4MB
        SoftReference<byte[]> hugeBlockRef3 = new SoftReference<>(hugeBlock3);
        hugeBlock3 = null;

        byte[] hugeBlock4 = new byte[4 * 1024 * 1024]; // 4MB
        SoftReference<byte[]> hugeBlockRef4 = new SoftReference<>(hugeBlock4);
        hugeBlock4 = null;

        System.out.println("hugeBlockRef1 = " + hugeBlockRef1.get());
        System.out.println("hugeBlockRef2 = " + hugeBlockRef2.get());
        System.out.println("hugeBlockRef3 = " + hugeBlockRef3.get());
        System.out.println("hugeBlockRef4 = " + hugeBlockRef4.get());
    }
}

在这段代码中,我们首先在堆中创建了四个4M大小的内存块,然后通过SoftReference为这些对象创建软引用,并将这些对象的强引用都去掉使得这4个对象都变成 soft reachable,然后在启动JVM的时候通过JVM参数-Xmx10M限制最大内存为10M,然后运行程序观察结果:

hugeBlockRef1 = null
hugeBlockRef2 = null
hugeBlockRef3 = null
hugeBlockRef4 = [B@610455d6

可以看到,由于hugeBlock1hugeBlock2hugeBlock3这3个对象占用的内存加起来超过了10M,所以触发了SoftReference引用对象被回收的条件,这三个对象被回收以后导致调用SoftReferenceget()方法的时候返回null(需要注意的是,这里说的对象被回收指的是通过软引用获取不到这些对象了,但是实际这些对象占用的空间并不一定马上释放,JVM会在未来释放这些内存空间,在介绍虚引用和finalize()的时候我们会重点讨论这块内容)。

弱引用

弱引用 也叫 Weak Reference,在Java中通过WeakReference类型表示,是Java引入的另外一种引用类型。弱引用和强引用的主要区别是:一个对象即使有弱引用关联,如果这个对象是 弱可达(weakly reachable)的,那么垃圾收集器在下一个GC周期就可以将这个对象回收掉,这个行为不同于SoftReferenceweak reachable 的对象会在下一个GC周期被垃圾回收器回收掉,即使当前有足够的内存,而 soft reachable 的对象只有当内存不够的时候才会触发回收.所以从引用的强弱关系来看,SoftReference > WeakReference

那么,在什么情况下一个对象是 weakly reachable 的呢,在Java的官方文档1里是这样定义的:

An object is weakly reachable if it is neither strongly nor softly reachable but can be reached by traversing a weak reference. When the weak references to a weakly-reachable object are cleared, the object becomes eligible for finalization.

也就是说,一个对象只要没有强引用关联和软引用关联,那么这个对象就满足 weakly reachable 的条件。一个 weakly reachable 的对象符合垃圾回收器标记为垃圾对象的条件,在未来会被垃圾回收器回收。注意这里的措辞,一个 weakly reachable 对象并不会马上被垃圾收集器回收,只有在下次GC周期到来的时候才会尝试回收,而在回收的时候也不一定能保证回收掉,因为这里有一个 finalization 过程:也就是调用对象的Object.finalize()方法对对象进行解构。在Object.finalize()方法中可能会将标记为垃圾的对象复活,这里我们先把finalize()的内容先放放,后面讲到虚引用PhantomReference的时候再回过头来讲下Java的finalize()

SoftReference引用一样,WeakReference引用类型也提供了2个构造方法和一个get()方法。用法和SoftReference一样,只是get()返回null的情况不同,也就是对象回收的时机不同。

public WeakReference(T referent);
public WeakReference(T referent, ReferenceQueue<? super T> q);
public T get();

下面是WeakReference的例子:

public class Test {
    public static void main(String ...args) {
        HugeBlock block = HugeBlock.sizeOf(4 * 1024 * 1024);
        WeakReference<HugeBlock> hugeBlockRef1 = new WeakReference<>(block);
        block = null;

        System.out.println("Before trigger gc, hugeBlockRef1 = " + hugeBlockRef1.get());

        System.gc(); // 触发GC

        System.out.println("After trigger gc, hugeBlockRef1 = " + hugeBlockRef1.get());
    }

    private static class HugeBlock {
        private byte[] block;

        private HugeBlock(int size) {
            this.block = new byte[size];
        }

        public static HugeBlock sizeOf(int size) {
            return new HugeBlock(size);
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
        }

        @Override
        public String toString() {
            return String.format("Byte Block [%d bytes]", block.length);
        }
    }
}

输出结果:

Before trigger gc, hugeBlockRef1 = Byte Block [4194304 bytes]
After trigger gc, hugeBlockRef1 = null

这里,我们通过System.gc()主动触发JVM的minor gc。可以观察到在触发gc以后WeakReference引用的对象被垃圾回收了。这次我们没有对JVM加-Xmx10M的内存限制,但是由于WeakReference的特点,弱引用的对象还是会被垃圾回收掉。

虚引用

虚引用 是Java四种引用类型中最特殊的一种引用类型,在Java中通过PhantomReference类来表示。PhantomReferenceWeakReference比较像,但是不同于WeakReference引用的对象只有在被引用的对象回收以后get()方法才返回null,而PhantomReference引用的get()方法将永远返回null,即使这个对象没有被垃圾回收器回收。这就是虚引用名字的由来,它又被称为 幽灵引用,因为这个引用对象不会返回被引用的对象,就像幽灵一样。

同样的,一个对象如果满足 phantom reachable,那么这个对象就可以被垃圾回收器回收。Java官方文档1phantom reachable 的定义如下:

An object is phantom reachable if it is neither strongly, softly, nor weakly reachable, it has been finalized, and some phantom reference refers to it.

一个对象如果既不是强引用,也不满足 soft reachableweakly reachable 的条件,并且这个对象已经被解构(执行过Object.finalize()方法),并且有PhantomReference引用指向它,那么这个对象就被认为是 phantom reachable

这里需要注意的一点是,只有PhantomReference引用的对象,在达到 phantom reachable 状态的时候,它的Object.finalize()是已经被执行过的,而对于前面说的SoftReferenceWeakReference引用的对象,它们在达到 soft reachableweakly reachable 状态的时候,虽然被标记为可以被垃圾回收器回收,但是它们的Object.finalize()方法还没执行,而且他们的Object.finalize()可能永远不会被执行,在后面介绍Object.finalize()方法的时候你将看到这一点。这个是PhantomReferenceWeakReference之间的第二大区别,同时也是PhantomReference和其他两个引用类型之间最重要的区别。

public T get();
public PhantomReference(T referent, ReferenceQueue<? super T> q);

可以发现PhantomReference的构造函数只有一个。由于PhantomReferenceget()方法不管被引用的对象有没有被回收,返回的都是null,所以PhantomReference必须需要配合ReferenceQueue一起来使用。

我们已经介绍完了Java中的四种引用类型.下面,我们来介绍下引用队列ReferenceQueue以及它的用途。

引用队列

介绍了4种引用类型,你可能会问Java引入强引用之外的3种引用类型的目的是什么?对于SoftReferenceWeakReference来说,我们通过检查get()的返回值是否为null来判断对象是否被回收了。但是对于PhantomReference来说,由于它的get()方法返回的永远是null,所以我们单纯使用PhantomReference好像并不能做任何事。

Java引入这三种引用类型的目的是为了让开发人员可以感知到Java垃圾回收器的行为,配合垃圾回收器管理Java中创建的对象。而怎么和垃圾回收器联系起来,就需要用到我们将要提到的引用队列ReferenceQueue

前面在介绍引用类型的时候,我们提到针对不同的引用类型,当引用的对象达到不同的可达状态的时候就会被垃圾回收器回收。比如:对于SoftReference引用的对象,当被引用的对象达到soft reachable的时候,在未来的某个时间就会被垃圾回收器回收。有时候,我们希望垃圾回收器回收对象的时候可以通知应用程序,告诉它:“你的对象被我回收了”,这个时候,ReferenceQueue就可以派上用场了(当然,如果没有ReferenceQueue,对于WeakReferenceSoftReference来说,要知道对象什么时候被回收也不是难事,只要定期检查get()的返回值是否为null就可以了,但是对于PhantomReference来说就办不到了)。

当垃圾回收器发现引用的对象可以被回收的时候,会将这些对象放到ReferenceQueue中。应用程序通过轮询ReferenceQueue就可以知道哪个引用的对象被回收了。将前面介绍的三种引用类型配合引用队列,应用程序就可以感知到对象什么时候被垃圾回收器回收,然后实现一些清理逻辑。比如在WeakHashMap的实现中,对过期entry的淘汰就依赖于ReferenceQueue;还有ThreadLocal中的哈希表也有类似的实现。

当引用的对象达到对应的可达性状态以后,如果这个引用是被注册的(构造函数中提供了引用队列),那么垃圾回收器在回收前会将这个引用对象(注意,不是被回收的对象,而是引用对象。所以引用对象其实是一个对象句柄,指向被回收的对象)放到ReferenceQueue中。

对于不同的引用类型,引用对象被放到ReferenceQueue的时机是不同的。对于SoftReference来说,当被引用的对象是 soft reachable 状态的时候,该对象的引用对象会在未来JVM内存不够导致触发GC的时候被放入ReferenceQueue中,同时在入队前会将被引用对象的引用去掉。对于WeakReference来说,当被引用的对象的状态是 weakly reachable 的时候,该引用对象会在下一次GC的时候被放入ReferenceQueue,和SoftReference一样,在入队前会将该引用类型对被引用对象的引用去掉。

reference_01

PhantomReference引用的对象在对象达到 phantom reachable 的时候会将该引用对象入队,但是PhantomReference的实现在JDK9之前比较特殊:它在入队的时候不会将对被引用对象的引用去掉,虽然通过PhantomReferenceget()方法拿不到被引用的对象。在JDK92中做了修改,PhantomReference入队的逻辑和前面两个引用类型一样,在入队前会将被引用对象的PhantomReference引用去掉。

Soft and weak references are automatically-cleared references (i.e. these references are cleared by the collector before it’s enqueued) whereas phantom reference is not an automatically-cleared reference. Instead, the get method of a phantom reference always returns null to ensure that the referent of a phantom reference may not be retrieved.

This proposes to make phantom references automatically-cleared reference as soft and weak references do.2

下面通过WeakReference配合ReferenceQueue来介绍下引用队列是如何使用的。

public class Test {
    public static void main(String ...args) throws Exception {
        ReferenceQueue<HugeBlock> queue = new ReferenceQueue<>();
        HugeBlock block = HugeBlock.sizeOf(4 * 1024 * 1024);
        WeakReference<HugeBlock> hugeBlockRef1 = new WeakReference<>(block, queue);
        block = null;

        System.out.println("Before trigger gc, hugeBlockRef1 = " + hugeBlockRef1 + ", hugeBlockRef.get() = " + hugeBlockRef1.get());

        System.gc(); // 触发GC

        System.out.println("After trigger gc, hugeBlockRef1 = " + hugeBlockRef1 + ", hugeBlockRef.get() = " + hugeBlockRef1.get());

        Reference<? extends HugeBlock> ref = queue.remove();
        System.out.println("Get ref from queue: " + ref);
    }

    private static class HugeBlock {
        private byte[] block;

        private HugeBlock(int size) {
            this.block = new byte[size];
        }

        public static HugeBlock sizeOf(int size) {
            return new HugeBlock(size);
        }

        @Override
        public String toString() {
            return String.format("Byte Block [%d bytes]", block.length);
        }
    }
}

输出结果:

Before trigger gc, hugeBlockRef1 = java.lang.ref.WeakReference@610455d6, hugeBlockRef.get() = Byte Block [4194304 bytes]
After trigger gc, hugeBlockRef1 = java.lang.ref.WeakReference@610455d6, hugeBlockRef.get() = null
Get ref from queue: java.lang.ref.WeakReference@610455d6

可以看到,在创建WeakReference对象的时候传入queue,在通过System.gc()触发GC的时候我们可以从queue中取到被标记为回收的对象(需要注意的是,对于WeakReference来说,一个对象的WeakReference引用放入引用队列并不代表这个对象已经被GC了,只是说这个对象满足 weak reachable 的条件,这个对象是否被垃圾回收器GC,什么时候执行GC都是未确定的,有时候可能这个对象根本就没有被垃圾回收掉,比如在后面介绍finalize()的时候你将会看到一个对象在进入了引用队列以后又是如何存活在JVM中的)。

虚引用和finalize( )

Java不像C++那样有析构函数,因为Java是支持垃圾回收的语言,所以一般情况下并不需要析构函数的概念。但是Java的Object类上有一个finalize()方法,在对象被回收的时候垃圾回收器会执行这个finalize()方法,很多开发者会将这个方法认为是Java的析构函数,通过在类中重载finalize()方法来实现具体的析构逻辑。但是finalize()方法的行为可能并不是大家想当然的那样。

finalize( )的缺陷

首先,JVM并不能保证方法的finalize()方法什么时候会被执行。唯一可以确定的是,当垃圾回收器发现一个对象不可达的以后(在第一轮GC周期的时候),会将这个对象放入一个finalize队列中,由另外一个线程从队列中读取这个对象并执行它的finalize()方法,当一个对象的finalize()方法被执行完成以后,会再次进行第二轮的GC,以检查执行完finalize()方法以后的这个对象是否还可达。

这里垃圾回收器要对被回收的对象检查两轮是有原因的,因为一个对象的实现者可以在finalize()方法中重新对这个对象创建一个强引用,导致这个本来应该被回收的对象又复活了。所以垃圾回收器需要在执行完finalize()方法以后再次检查对象的可达性,如果这个对象被复活,那么这个对象就不能被回收。由于JVM中规定一个对象的finalize()只能被执行一次,所以对象在下次GC的时候将不会执行它的finalize()方法。所以,不正确的finalize()实现可能会导致内存泄露。由于finalize()方法存在这个问题,所以官方不建议通过实现finalize()来做资源清理工作。

finalize()除了刚才提到的这个问题,更要命的是JVM不能保证对象的finalize()方法一定执行。JVM可能直到退出也没有执行对象的finalize()方法。虽然JVM提供了System.runFinalization()System.runFinalizersOnExit()以及Runtime.runFinalizersOnExit()方法来告知JVM执行对象的finalize()方法,但是这仍旧存在不执行的情况,而且这些方法由于存在潜在的安全问题和死锁风险已经被官方废弃了。另一方面,由于finalize()方法是在GC阶段被执行的,所以如果在finalize()的实现中包含了耗时的阻塞操作,会导致GC时间变长(虽然finalize()是异步执行的,但是总的GC时间会变长)。

除了上面我们列举的一些点,finalize()还存在一些其他的问题,所以官方不建议通过finalize()来实现资源释放,如果要编写资源释放逻辑,更好的方案是使用后面会讲到的PhantomReference配合上ReferenceQueue来实现,不过在这之前,我们先来看一个有意思的问题。

我们前面提到了当一个注册的WeakReference(构造的时候提供了ReferenceQueue)指向的对象变成 weakly reachable 的时候,这个引用对象就会被放入引用队列中。而从 weakly reachable 的定义来看,一个对象如果没有强引用和软引用而只有弱引用,这个对象就会变成 weakly reachable,这个时候这个对象的弱引用对象就会被放到队列中,并删除指向这个被引用对象的弱引用。在未来的某个时间点这个被引用对象会进入GC流程,也就是上面提到的2次GC检查和执行finalize()方法。基于这个 weakly reference 定义,我们就可以发现一个有意思的事情:如果这个对象的弱引用已经进入队列了,但是在执行这个对象的finalize()的时候复活了这个对象,导致这个对象最后仍旧存活在JVM中。下面来看例子:

public class Test {
    public static void main(String ...args) throws Exception {
        ReferenceQueue<HugeBlock> queue = new ReferenceQueue<>();
        HugeBlock block = HugeBlock.sizeOf(4 * 1024 * 1024);
        WeakReference<HugeBlock> hugeBlockRef1 = new WeakReference<>(block, queue);
        block = null;

        System.out.println("Before trigger gc, hugeBlockRef1 = " + hugeBlockRef1 + ", hugeBlockRef.get() = " + hugeBlockRef1.get());

        System.gc(); // 触发GC

        System.out.println("After trigger gc, hugeBlockRef1 = " + hugeBlockRef1 + ", hugeBlockRef.get() = " + hugeBlockRef1.get());

        Reference<? extends HugeBlock> ref = queue.remove();
        System.out.println("Get ref from queue: " + ref);

        Thread.sleep(1000); // finalize是异步执行的,需要等一段时间
        System.out.println("After run finalize, block = " + HugeBlock.saved);
    }

    private static class HugeBlock {
        public static HugeBlock saved;

        private byte[] block;

        private HugeBlock(int size) {
            this.block = new byte[size];
        }

        public static HugeBlock sizeOf(int size) {
            return new HugeBlock(size);
        }

        @Override
        public String toString() {
            return String.format("Byte Block [%d bytes]", block.length);
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();

            System.out.println("Run finalize()");
            saved = this;
        }
    }
}

输出结果:

Before trigger gc, hugeBlockRef1 = java.lang.ref.WeakReference@610455d6, hugeBlockRef.get() = Byte Block [4194304 bytes]
After trigger gc, hugeBlockRef1 = java.lang.ref.WeakReference@610455d6, hugeBlockRef.get() = null
Run finalize()
Get ref from queue: java.lang.ref.WeakReference@610455d6
After run finalize, block = Byte Block [4194304 bytes]

可以看到,当我们把block对象变成 weakly reachable 以后,触发GC使得对block对象的弱引用hugeBlockRef1被放入引用队列queue中,但是由于我们重载了HugeBlockfinalize()方法,导致在执行finalize()的时候block对象被复活了。但是hugeBlockRef1已经被放入到了queue中。

按照 weakly reachable 的定义(当然也包括 soft reachable),我们不能通过从队列中读取引用对象来确认对象在未来一定被GC。那有没有办法保证,只要引用对象进入队列,就可以确认这个对象不会被复活,在未来一定会被GC掉呢?那这个就需要用到PhantomReference

使用虚引用

我们知道一个对象如果变成了 phantom reachable,这个对象就会被放入ReferenceQueue。而对于 phantom reachable 的定义,满足的条件中和其他两个引用最大的区别就是在 phantom reachable 的达成条件中多了一个:对象必须是已经执行完finalize()方法并且除了虚引用之外没有任何引用的。也就是说,如果一个本来不可达的对象在finalize()中复活以后,它就不满足PhantomReference的进队条件,不像WeakReferenceSoftReference那样是先进队后执行finalize()方法,而且PhantomReferenceget()方法永远返回null。这就可以保证从引用队列中取出的虚引用对象,它原先指向的对象肯定是没有任何引用了,我们可以放心地释放对象对应的资源。

public class Test {
    public static void main(String ...args) throws Exception {
        ReferenceQueue<HugeBlock> queue = new ReferenceQueue<>();
        HugeBlock block = HugeBlock.sizeOf(4 * 1024 * 1024);
        PhantomReference<HugeBlock> hugeBlockRef1 = new PhantomReference<>(block, queue);
        block = null;

        System.out.println("Before trigger gc, hugeBlockRef1 = " + hugeBlockRef1 + ", hugeBlockRef.get() = " + hugeBlockRef1.get());

        System.gc(); // 触发GC

        System.out.println("After trigger gc, hugeBlockRef1 = " + hugeBlockRef1 + ", hugeBlockRef.get() = " + hugeBlockRef1.get());
        
        System.gc(); // 再次触发GC,因为执行完finalize()方法以后,需要在下一个GC周期中检查对象是否是存活状态的

        Reference<? extends HugeBlock> ref = queue.remove();
        System.out.println("Get ref from queue: " + ref);
    }

    private static class HugeBlock {
        private byte[] block;

        private HugeBlock(int size) {
            this.block = new byte[size];
        }

        public static HugeBlock sizeOf(int size) {
            return new HugeBlock(size);
        }

        @Override
        public String toString() {
            return String.format("Byte Block [%d bytes]", block.length);
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("Run finalize()");
        }
    }
}

输出结果:

Before trigger gc, hugeBlockRef1 = java.lang.ref.PhantomReference@610455d6, hugeBlockRef.get() = null
Run finalize()
After trigger gc, hugeBlockRef1 = java.lang.ref.PhantomReference@610455d6, hugeBlockRef.get() = null
Get ref from queue: java.lang.ref.PhantomReference@610455d6

代码中我们触发了两次System.gc(),因为前面提到了垃圾回收器需要在执行完finalize()以后再次检查对象是否可达,如果对象不可达,那么之前指向这个不可达对象的引用对象就会被入队。我们可以尝试在finalize()方法中将对象复活:

public class Test {
    public static void main(String ...args) throws Exception {
        ReferenceQueue<HugeBlock> queue = new ReferenceQueue<>();
        HugeBlock block = HugeBlock.sizeOf(4 * 1024 * 1024);
        PhantomReference<HugeBlock> hugeBlockRef1 = new PhantomReference<>(block, queue);
        block = null;

        System.out.println("Before trigger gc, hugeBlockRef1 = " + hugeBlockRef1 + ", hugeBlockRef.get() = " + hugeBlockRef1.get());

        System.gc(); // 触发GC

        System.out.println("After trigger gc, hugeBlockRef1 = " + hugeBlockRef1 + ", hugeBlockRef.get() = " + hugeBlockRef1.get());
        
        System.gc(); // 再次触发GC,因为执行完finalize()方法以后,需要在下一个GC周期中检查对象是否是存活状态的

        Reference<? extends HugeBlock> ref = queue.remove();
        System.out.println("Get ref from queue: " + ref);
    }

    private static class HugeBlock {
        public static HugeBlock saved;

        private byte[] block;

        private HugeBlock(int size) {
            this.block = new byte[size];
        }

        public static HugeBlock sizeOf(int size) {
            return new HugeBlock(size);
        }

        @Override
        public String toString() {
            return String.format("Byte Block [%d bytes]", block.length);
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("Run finalize()");

            saved = this; // 复活对象
        }
    }
}

输出结果:

Before trigger gc, hugeBlockRef1 = java.lang.ref.PhantomReference@610455d6, hugeBlockRef.get() = null
After trigger gc, hugeBlockRef1 = java.lang.ref.PhantomReference@610455d6, hugeBlockRef.get() = null
Run finalize()

当执行这段代码的时候,我们会发现JVM阻塞在了queue.remove();上,因为对象block在执行完finalize()以后仍旧存活,所以它的引用对象不会被入队,导致queue.remove()操作阻塞。同样,我们也可以将上面第二个System.gc()去掉,这样由于达不到2次GC的条件(前提是内存足够,不会触发GC),所以我们仍旧会阻塞在queue.remove()调用上,这里就不放代码了,感兴趣的读者可以自己试试。

相对于Java的Object.finalize(),使用PhantomReferenceReferenceQueue可以给开发者提供更加灵活的资源释放解决方案,而且引用对象入队是异步的,资源释放不会影响到垃圾回收的过程。同时使用引用队列可以使实现资源清理的代码和垃圾回收器之间松耦合。

总结

到这里,我们大致已经介绍完了Java中四种引用类型,重点介绍了软弱虚三种引用类型的概念和作用。通过介绍Java中finalize()方法的缺陷,引出PhantomReference这种特殊的引用类型是如何配合引用队列来代替finalize()工作的。

TOP