在工作中99%我们用的都是强引用,但是面试中99%问的都是后面三种引用类型的区别。
首先,我们来新建一个类,叫做Zhouyu:
public class Zhouyu{private byte[] objects = new byte[1024*1024*1024]; // 1G}
一个Zhouyu对象,占用大概1G内存,所以,假设堆内存最大只有2000M( -Xmx2000M),如果我们只生成一个Zhouyu对象,是不会出现OOM的:
public static void main(String[] args) throws IOException {Zhouyu zhouyu1 = new Zhouyu();}
如果我们生成两个Zhouyu对象,就会出现OOM了:
public static void main(String[] args) throws IOException {Zhouyu zhouyu1 = new Zhouyu();Zhouyu zhouyu2 = new Zhouyu();}
以上的zhouyu1,zhouyu2就是强引用。
对于一个对象,只要有强引用指向了它,它就不会被垃圾回收掉,就算堆空间已经不够了,也不会被垃圾回收掉,而是直接抛OOM。
而软引用就不一样,对于一个对象,如果只有软引用指向了它,在堆空间足够时,它不会被垃圾回收掉,但是如果堆空间不够了,那么就回收该对象。
我们可以通过SoftReference来定义一个软引用,并指向一个Zhouyu对象,并通过get()方法来获取软引用所指向的对象。
SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());System.out.println(zhouyuSoftReference.get());
如果是下面的代码,照样会出现OOM。
Zhouyu zhouyu = new Zhouyu();SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());System.out.println(zhouyuSoftReference.get());
如果是下面的代码,则不会出现OOM。
SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());System.out.println(zhouyuSoftReference.get());Zhouyu zhouyu = new Zhouyu();
因为在创建第二个Zhouyu对象时,堆内存已经不够了,但是由于第一个Zhouyu对象只有一个软引用指向了它,此时就被垃圾回收掉了,所以如果紧接着来get,则会拿到null:
SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());System.out.println(zhouyuSoftReference.get());Zhouyu zhouyu = new Zhouyu();System.out.println(zhouyuSoftReference.get()); // null
我们可以手动调用gc来测试,比如:
SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());System.out.println(zhouyuSoftReference.get());System.out.println(zhouyuSoftReference.get());
连续两次get,都能正常得到对象,如果在中间主动触发一次gc:
SoftReference<Zhouyu> zhouyuSoftReference = new SoftReference<Zhouyu>(new Zhouyu());System.out.println(zhouyuSoftReference.get());System.gc();Thread.sleep(1000); // 保证gc执行完System.out.println(zhouyuSoftReference.get());
依然都能正常得到对象,因为软引用指向的对象,只有在堆内存不够时,才会被回收掉。
和软引用比较类似的是弱引用,比如:
WeakReference<Zhouyu> zhouyuWeakReference = new WeakReference<Zhouyu>(new Zhouyu());System.out.println(zhouyuWeakReference.get());System.out.println(zhouyuWeakReference.get());
两次都能获得对象,但是:
WeakReference<Zhouyu> zhouyuWeakReference = new WeakReference<Zhouyu>(new Zhouyu());System.out.println(zhouyuWeakReference.get());System.gc();Thread.sleep(1000);System.out.println(zhouyuWeakReference.get());
第二次get将得到null,表示弱引用指向的对象,一旦进行了垃圾回收就会被回收掉。
对于强引用、软引用、弱引用,先总结一下:
- 强引用指向的对象,垃圾回收也回收不掉
- 软引用指向的对象,只有在堆内存不够的情况下,才能被垃圾回收掉
- 弱引用指向的对象,只要进行了垃圾回收,就会被回收掉
而虚引用,跟上述三种引用类型不太一样。
以下是虚引用的用法:
ReferenceQueue queue = new ReferenceQueue();PhantomReference<Zhouyu> zhouyuPhantomReference = new PhantomReference<>(new Zhouyu(), queue);System.out.println(zhouyuPhantomReference.get());
我们发现在使用虚引用指向一个对象时,还得指定一个ReferenceQueue,ReferenceQueue的作用等会再解释,更重要的是,调用get方法返回的是null。
这是因为,虚引用是真的很虚,相当于没有,我们是无法通过虚引用来获得对象的,那虚引用的作用是什么呢?这就需要联系到刚刚的ReferenceQueue了,对于虚引用所指向的对象,当出现垃圾回收时,该对象就会被回收掉,同时会把该对象对于的虚引用添加到ReferenceQueue中,比如:
ReferenceQueue queue = new ReferenceQueue();PhantomReference<Zhouyu> zhouyuPhantomReference = new PhantomReference<>(new Zhouyu(), queue);System.out.println(zhouyuPhantomReference.get()); // nullSystem.out.println(queue.poll()); // nullSystem.gc();Thread.sleep(1000);System.out.println(queue.poll()); // java.lang.ref.PhantomReference@73a3d5c3
所以虚引用需要和ReferenceQueue配合使用,通过ReferenceQueue可以知道某个对象是否被垃圾回收掉了。
虚引用比较经典的使用场景是NIO中的ByteBuffer,我们可以使用一下代码来分配直接内存:
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
此处有一个强引用byteBuffer指向了ByteBuffer对象,但是一旦我们使得ByteBuffer对象被垃圾回收掉:
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);byteBuffer = null;System.gc();
可以发现直接内存也释放了,这是怎么做到的?
实际上在allocateDirect方法中,用到了一个类Cleaner,这个类是JDK所提供的,它继承了PhantomReference,也就是虚引用,我们可以这么来使用Cleaner:
Zhouyu zhouyu = new Zhouyu();Cleaner zhouyuCleaner = Cleaner.create(zhouyu, new Runnable() {@Overridepublic void run() {System.out.println("1111");}});zhouyu = null;System.gc();
利用Cleaner生成了一个虚引用指向了zhouyu对象,然后通过gc把zhouyu对象回收掉了,但是在真正回收之前会调用Cleaner中所设置的Runnable对象中的run方法,相当于一种回调。
而在ByteBuffer中,就是利用的Cleaner机制,对于ByteBuffer对象,有一个强引用byteBuffer指向了它,同时也有一个Cleaner虚引用指向了它,同时指定了一个Deallocator对象作为回调,Deallocator中的run方法会进行直接内存的释放,只要把强引用byteBuffer清空,一定进行了垃圾回收,就会回收ByteBuffer对象并指向Deallocator中的run方法。
