四种引用类型
JDK1.2 之前,一个对象只有“已被引用”和"未被引用"两种状态,这将无法描述某些特殊情况下的对象,比如,当内存充足时需要保留,而内存紧张时才需要被抛弃的一类对象。
所以在 JDK.1.2 之后,Java 对引用的概念进行了扩充,将引用分为了:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4 种,这 4 种引用的强度依次减弱(强软弱虚)。
强引用
强引用即某个变量名直接指向了某个对象.例如下面这样:
Object o = new Object();
对于强引用,一般不需要我们进行回收,Java的gc(garbage collection)将会自动帮助我们回收不再指向其他对象的变量。来看下面两种情况:
第一种情况:对象置空
第二种情况:不进行操作,业务流程正常结束
可以发现两者唯一的区别就是一个是变量指向空,另一个则是变量正常等待程序结束,可以发现第一种情况调用了finalize方法,而第二种方法并没有。
原因是因为:
对于第一种情况,Java对象指向空之后,gc会自动过来对空对象进行回收,防止内存浪费,此时硬件的控制权还在JVM手上,JVM将会在回收完毕空引用之后才会认为所有的任务结束,JVM才会将资源返回给OS。
而对于第二种情况,程序正常退出,在主动的调用gc的时候,JVM此时并没有发现有空引用,那么JVM正常结束,将控制权返回给OS,程序退出,因此,第二种情况并没有发生finalize函数的调用。
只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了。
这里我设定了最大堆内存为20M,因此创建两个12M的大小的数组的时候会直接报错。
软引用
软引用是一种特殊的包装引用,下面来看一个例子.
SoftReference<byte[]> sr = new SoftReference<>(new byte[1024*1024*10]);
下面这行代码中,泛型指定为一个字节数组。在创建这个对象的时候,构造函数中的参数指定为了一个数组,那么这个对象内部的属性将会指向这个数组,此时就产生了一个软引用。
同时,将这个SoftReference对象赋值给sr这个变量的时候,有产生了一个强引用。
即sr直接指向的对象SoftReference是一个强引用,创建SofrReference时为其赋值的数组,是一个软引用。
可以发现调用垃圾回收器之后,软引用并没有被回收,这也就说明了软引用的一个特点。
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
继续来看,我现在为VM设定了最大堆内存为20M.
此时我们再来运行程序
可以发现软引用居然变为了null。说明他被垃圾回收器回收了。
那么为什么会这样子?
首先,我设定了VM的最大堆内存为20M,而我们这个软引用的字节数组占了10M,同时JVM本身也需要占6-7M的内存,那么此时只剩有3-4M的内存。而此时我们又创建了一个强引用,创建了一个12M的字节数组,那么由上面的概念可以知道,软引用不是必须的,因此JVM就把软引用释放了用于存放强引用字节数组。因此再一次试图获取软引用引用的时候,返回的值就是null了。
因此软引用就是一个可有可无的人物,内存足够,不释放你,内存不够,那么直接拜拜。
也正是由于这一可有可无的特性,使得软引用非常适合用作于缓存。
即有你我可能可以干的更好,但是没你我不一定干不了。
虚引用
下面这个程序开启了两个线程,一个线程用于不断的向内存中创建数组,另一个内存则不断检测队列中是否有被释放掉的对象。
由于第一个线程不断的向内存中添加数据,那么当内存溢出的时候,就需要是否虚引用对象,即这个StrongReferenceTest,而这个对象会被放入到后面的QUEUE中,此时垃圾回收器将会回收这个对象。而另一个线程也正是用于监控这个QUEUE队列中是否有被释放掉的对象。如果有,那么QUEUE不为空,进行判断后将会打印语句。
先来解释一下这一行代码
PhantomReference<StrongReferenceTest> phantomReference = new PhantomReference<>(new StrongReferenceTest(), QUEUE);
我们创建了一个虚引用对象,并且指定其中的类型为StrongReferenceTest(当然这里可以随意指定),同时还指定了一个QUEUE队列。此时虚引用指向的这个对象StrongReferenceTest在被回收的时候会被放入到这个QUEUE队列中。而这个队列的作用就是供垃圾回收器特殊处理。
public class PhantomReferenceTest { public static final List<Object> LIST = new LinkedList<>(); public static final ReferenceQueue<StrongReferenceTest> QUEUE = new ReferenceQueue<>(); public static void main(String[] args) { PhantomReference<StrongReferenceTest> phantomReference = new PhantomReference<>(new StrongReferenceTest(), QUEUE); System.out.println(phantomReference.get()); ByteBuffer b = ByteBuffer.allocateDirect(1024); new Thread(() -> { while (true) { LIST.add(new byte[1024 * 1024]); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(phantomReference.get()); } }).start(); new Thread(() -> { while (true) { Reference<? extends StrongReferenceTest> poll = QUEUE.poll(); if (poll != null) { System.out.println("---虚引用对象被jvm回收了---" + poll); } } }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
与上面一样,我们设定最大堆内存为20M,然后运行程序
可以发现发生了内存溢出,这很正常,因为内存就那么大,并且你还在不断的创建数组 ,内存一定会爆掉。
但是也可以注意到,虚引用对象调用get方法返回的一直是null,也就是我们想用这个StrongReferenceTest对象是获取不到的,只能是在StrongReferenceTest对象被回收的时候通知了一下垃圾回收器来回收这个对象。那么,就这样一个无法获取对象,只能在内存不够的时候通知垃圾回收器来回收特定对象的PhantomReference对象有什么用呢?
这就需要提到IO包了(现在叫NIO),我们在网络上读取数据的时候,数据首先被读入到网卡内,网卡再将数据写入到OS中的内存中,内存再把数据读入到JVM堆中的内存,也就是一个Buffer缓冲区,这个时候我们就可以对这个缓冲区的数据进行处理了。但是明显有一个问题,就是JVM本身也是一个内存,那么从OS的内存把数据写到JVM的内存不就多此一举,造成了性能浪费吗,因此就有了NIO,NIO使用的是直接内存,也就是JVM中的Buffer不要了,直接去访问OS中的内存,Java也提供了DirectBuffer这个对象来帮助我们直接访问内存,但是这部分的内存很明显,不归垃圾回收器管理,而是由OS管理,不由垃圾回收器管理,那么这一段内存就没办法被清理了。因此,对于这部分内存的释放,就需要使用虚引用,虚引用指向操作这部分内存的对象,对他们进行特殊处理,在需要释放他们的时候,将他们放入到QUEUE中,这也就是虚引用的使用。
这里,虚引用难以理解,但是也不太重要,当然除非你需要手写直接内存,当然,一般用不到。
弱引用(最重要)
弱引用中先存放了一个软引用,这个软引用指向StrongReferenceTest,此时通过弱引用调用get方法可以获得到其内部软引用指向的对象,但是我们在调用垃圾回收器之后,垃圾回收器直接回收了这个弱引用内部的软引用,之后我们再次调用弱引用的get方法可以发现返回的已经是null了。
因此,垃圾回收器看到弱引用都是直接回收,就把他当作一个垃圾一样。
假设一个对象被一个强引用和一个弱引用共同指向,那么在这个强引用还在的时候,这个对象还能保留,但是如果强引用没了,只剩弱引用,只要垃圾回收器看到了,那么这个对象直接被回收。
那么,弱引用到底有什么用呢?
这就不得不提到ThreadLocal这个类了。
来看下面的这一份代码
public class ThreadLocalTest { private static ThreadLocal<User> tl = new ThreadLocal<>(); public static void main(String[] args) { new Thread(()->{ try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } tl.set(new User()); System.out.println(tl.get()); }).start(); new Thread(()->{ try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(tl.get()); }).start(); } }
创建了两个线程,第一个线程存放进去User对象,第二个线程试图取出这个类中的User对象。
结果就是第一个线程取出了这个对象,但是第二个线程取出这个对象的时候发现居然是null。
明明是同一个类中的对象,为什么第二个线程取出对象的时候返回的居然是null了呢。
其实是因为ThreadLocal类中的set方法是在当前线程中对当前线程的ThreadLocalMap进行数据存放,因此new出来两个线程的时候,这两个线程的数据是无法互相访问的。
这里的this对象,也就是属性声明时候声明出来的tl。
这个set方法中有一个Entry,而这个Entry如下,继承了弱引用类
也就是线程Thread中的tl首先指向了ThreadLocal这个类,并且ThreadLocal这个类中的ThreadLocalMap也指向了ThreadLocal,因此当我们需要释放tl的时候,也就是tl=null的时候,由于还有一个ThreadLocalMap对象指向ThreadLocal,那么只要这个线程不结束,ThreadLocal就不会被释放,更危险的就是线程池中,一个线程用完放回去,另一个线程又继续使用刚才的线程,然后继续向Map中设定值,那么势必造成问题。
但是如果我们将其设定为了弱引用,那么只要强引用tl=null之后,这个ThreadLocalMap这个弱引用自然会被垃圾回收器回收,也就是key=null了,但是这样子key指向的对象不就无法释放了吗,此时就有一块空间无法被释放,就造成了内存泄漏问题。因此我们需要使用ThreadLocal的remove方法,这个方法将会释放map中所有的数据。
因此不再使用ThreadLocal之后,应该尽可能的使用remove方法释放内存数据。
使用场景
强引用不必多说。
弱引用用于ThreadLocal中用于防止内存泄漏。
软引用用于缓存技术。
虚引用用于通知垃圾回收器回收直接内存中的数据。