在工作中99%我们用的都是强引用,但是面试中99%问的都是后面三种引用类型的区别。

    首先,我们来新建一个类,叫做Zhouyu:

    1. public class Zhouyu{
    2. private byte[] objects = new byte[1024*1024*1024]; // 1G
    3. }

    一个Zhouyu对象,占用大概1G内存,所以,假设堆内存最大只有2000M( -Xmx2000M),如果我们只生成一个Zhouyu对象,是不会出现OOM的:

    1. public static void main(String[] args) throws IOException {
    2. Zhouyu zhouyu1 = new Zhouyu();
    3. }

    如果我们生成两个Zhouyu对象,就会出现OOM了:

    1. public static void main(String[] args) throws IOException {
    2. Zhouyu zhouyu1 = new Zhouyu();
    3. Zhouyu zhouyu2 = new Zhouyu();
    4. }

    以上的zhouyu1,zhouyu2就是强引用。

    对于一个对象,只要有强引用指向了它,它就不会被垃圾回收掉,就算堆空间已经不够了,也不会被垃圾回收掉,而是直接抛OOM。

    而软引用就不一样,对于一个对象,如果只有软引用指向了它,在堆空间足够时,它不会被垃圾回收掉,但是如果堆空间不够了,那么就回收该对象。

    我们可以通过SoftReference来定义一个软引用,并指向一个Zhouyu对象,并通过get()方法来获取软引用所指向的对象。

    1. SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());
    2. System.out.println(zhouyuSoftReference.get());

    如果是下面的代码,照样会出现OOM。

    1. Zhouyu zhouyu = new Zhouyu();
    2. SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());
    3. System.out.println(zhouyuSoftReference.get());

    如果是下面的代码,则不会出现OOM。

    1. SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());
    2. System.out.println(zhouyuSoftReference.get());
    3. Zhouyu zhouyu = new Zhouyu();

    因为在创建第二个Zhouyu对象时,堆内存已经不够了,但是由于第一个Zhouyu对象只有一个软引用指向了它,此时就被垃圾回收掉了,所以如果紧接着来get,则会拿到null:

    1. SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());
    2. System.out.println(zhouyuSoftReference.get());
    3. Zhouyu zhouyu = new Zhouyu();
    4. System.out.println(zhouyuSoftReference.get()); // null

    我们可以手动调用gc来测试,比如:

    1. SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());
    2. System.out.println(zhouyuSoftReference.get());
    3. System.out.println(zhouyuSoftReference.get());

    连续两次get,都能正常得到对象,如果在中间主动触发一次gc:

    1. SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());
    2. System.out.println(zhouyuSoftReference.get());
    3. System.gc();
    4. Thread.sleep(1000); // 保证gc执行完
    5. System.out.println(zhouyuSoftReference.get());

    依然都能正常得到对象,因为软引用指向的对象,只有在堆内存不够时,才会被回收掉

    和软引用比较类似的是弱引用,比如:

    1. WeakReference<Zhouyu> zhouyuWeakReference = new WeakReference<Zhouyu>(new Zhouyu());
    2. System.out.println(zhouyuWeakReference.get());
    3. System.out.println(zhouyuWeakReference.get());

    两次都能获得对象,但是:

    1. WeakReference<Zhouyu> zhouyuWeakReference = new WeakReference<Zhouyu>(new Zhouyu());
    2. System.out.println(zhouyuWeakReference.get());
    3. System.gc();
    4. Thread.sleep(1000);
    5. System.out.println(zhouyuWeakReference.get());

    第二次get将得到null,表示弱引用指向的对象,一旦进行了垃圾回收就会被回收掉。

    对于强引用、软引用、弱引用,先总结一下:

    1. 强引用指向的对象,垃圾回收也回收不掉
    2. 软引用指向的对象,只有在堆内存不够的情况下,才能被垃圾回收掉
    3. 弱引用指向的对象,只要进行了垃圾回收,就会被回收掉

    而虚引用,跟上述三种引用类型不太一样。

    以下是虚引用的用法:

    1. ReferenceQueue queue = new ReferenceQueue();
    2. PhantomReference<Zhouyu> zhouyuPhantomReference = new PhantomReference<>(new Zhouyu(), queue);
    3. System.out.println(zhouyuPhantomReference.get());

    我们发现在使用虚引用指向一个对象时,还得指定一个ReferenceQueue,ReferenceQueue的作用等会再解释,更重要的是,调用get方法返回的是null。

    这是因为,虚引用是真的很虚,相当于没有,我们是无法通过虚引用来获得对象的,那虚引用的作用是什么呢?这就需要联系到刚刚的ReferenceQueue了,对于虚引用所指向的对象,当出现垃圾回收时,该对象就会被回收掉,同时会把该对象对于的虚引用添加到ReferenceQueue中,比如:

    1. ReferenceQueue queue = new ReferenceQueue();
    2. PhantomReference<Zhouyu> zhouyuPhantomReference = new PhantomReference<>(new Zhouyu(), queue);
    3. System.out.println(zhouyuPhantomReference.get()); // null
    4. System.out.println(queue.poll()); // null
    5. System.gc();
    6. Thread.sleep(1000);
    7. System.out.println(queue.poll()); // java.lang.ref.PhantomReference@73a3d5c3

    所以虚引用需要和ReferenceQueue配合使用,通过ReferenceQueue可以知道某个对象是否被垃圾回收掉了。

    虚引用比较经典的使用场景是NIO中的ByteBuffer,我们可以使用一下代码来分配直接内存:

    1. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);

    此处有一个强引用byteBuffer指向了ByteBuffer对象,但是一旦我们使得ByteBuffer对象被垃圾回收掉:

    1. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
    2. byteBuffer = null;
    3. System.gc();

    可以发现直接内存也释放了,这是怎么做到的?

    实际上在allocateDirect方法中,用到了一个类Cleaner,这个类是JDK所提供的,它继承了PhantomReference,也就是虚引用,我们可以这么来使用Cleaner:

    1. Zhouyu zhouyu = new Zhouyu();
    2. Cleaner zhouyuCleaner = Cleaner.create(zhouyu, new Runnable() {
    3. @Override
    4. public void run() {
    5. System.out.println("1111");
    6. }
    7. });
    8. zhouyu = null;
    9. System.gc();

    利用Cleaner生成了一个虚引用指向了zhouyu对象,然后通过gc把zhouyu对象回收掉了,但是在真正回收之前会调用Cleaner中所设置的Runnable对象中的run方法,相当于一种回调。

    而在ByteBuffer中,就是利用的Cleaner机制,对于ByteBuffer对象,有一个强引用byteBuffer指向了它,同时也有一个Cleaner虚引用指向了它,同时指定了一个Deallocator对象作为回调,Deallocator中的run方法会进行直接内存的释放,只要把强引用byteBuffer清空,一定进行了垃圾回收,就会回收ByteBuffer对象并指向Deallocator中的run方法。