首页>>后端>>java->Java四种引用类型:强、软、弱、虚

Java四种引用类型:强、软、弱、虚

时间:2023-11-30 本站 点击:0

Java中提供了四个级别的引用:强引用、软引用、弱引用、虚引用,除强引用以外,其他的引用类型在java.lang.ref包下有具体的实现,且均派生自java.lang.ref.Reference,如图所示:

可以看到除了软弱虚引用以外,Reference还有一个派生类Finalizer,该引用类型就是用于实现我们常说的finalize函数的。

一、引用队列ReferenceQueue

在介绍引用类型之前,先来介绍一个与所有引用类型都相关的一个东东:引用队列java.lang.ref.ReferenceQueue。

ReferenceQueue可以和软引用、弱引用、虚引用结合使用。关于ReferenceQueue,我们只需要知道最重要的一点:ReferenceQueue中存在的引用指向的对象不是被JVM回收了,就在回收的路上。

所以RerferenceQueue能干啥?既然我们知道了最重要的那一点,那么当JVM回收掉对象之后,就相当于发出了一个通知告诉我们XX被回收了,那么此时我们就可以给被回收的对象交代后事,当然交代后事这个动作也可以放在finalize()中去做,但是最好不要这么做!!

这里简单顺便提一下finalize的实现:每一个即将被回收并且包含finalize()函数的对象在正式回收前会被加入到叫做FinalizeThread线程的执行队列中,这个队列就是我们的ReferenceQueue,其中队列中的对象类型就是Finalizer,从上面的UML类图可以看出,Finalizer继承自FinalReference,每一个Finalizer包装了实际要被回收的对象,然后队列中的元素排队开始执行finalize函数,所以一个糟糕的finalize函数可能会使得对象长时间被Finalizer引用,而得不到释放,将长时间堆积在内存中,可能造成OOM,进一步增加GC压力

二、强引用

强引用就是程序中一般使用的引用类型,强引用的对象具有可触及、不会被回收的特点。例如:

StringBufferstr=newStringBuffer("StrongReference")

在上述代码中 str即StringBuffer实例的强引用,其中str局部变量分配在栈上,而StringBuffer实例分配在堆上(当然也可能是栈上分配),如果此时再执行如下代码:

StringBufferstr2=str;

那么此时StringBuffer对象实例就拥有两个引用。那么怎么让某个对象实例不再拥有强引用呢?那其实就是没有任何引用指向该实例即可:

str=null;str2=null;

强引用具有如下特点: 1)强引用可以直接访问目标对象 2)强引用所指向的对象在任何时候都不会被系统回收,即使OOM 3)基于第二点,所以强引用可能会导致内存泄漏

三、软引用

软引用对应实现为java.lang.ref.SoftReference,相比较强引用稍微弱一点,假如当堆内存空间不足时,则回收软引用对象。正应如此,软引用常可以用来做缓存功能。软引用还可以结合引用队列ReferenceQueue使用,如果软引用指向的对象实例被回收,则JVM会将此软引用加入到与之关联的ReferenceQueue中。

使用JVM参数 -Xms7M -Xmx7m -XX:+PrintGC 运行如下代码:

运行后结果:

在示例程序中,堆的大小为7M,并开启了PrintGC参数,用于发生GC时打印GC日志。接下来分为两部分:

第一部分:在main程序中首先新建了一个User对象,在User对象内部持有一个4M大小的字节数组,暂且就认为这个对象实例大小为4M(当然肯定大于4M),然后为新建的User实例建立软引用后,去除该User对象实例的强引用。此时通过System.gc()手动进行垃圾回收的触发之后,可见依然能够获取到user对象实例的内容,说明虽然发生了垃圾回收,但是其实此时内存充足,并不会回收软引用。 第二部分:紧接着第一部分,然后再次尝试分配4M大小的字节数组,由于我们的堆大小只有7M,所以此时肯定无法分配,系统将触发GC,触发GC之后,可以从回收日志看到大约回收了4M的空间(即我们的软引用对象),使得新分配的4M字节数组可以容纳。并且在这之后,软引用对象获取到的是NULL。

结论:当系统发生GC时,未必会回收软引用的对象,除非内存资源紧张不足时,软引用对象将被回收,所以软引用对象不会引起内存泄漏。

应用场景:缓存

四、弱引用

弱引用相比较软引用要稍微弱一点。当系统发生GC时,不管此时系统资源是否充足,都会对弱引用进行回收, 当然通常情况下垃圾回收线程的优先级比较低,并不一定会及时发现持有弱引用的对象。弱引用对应的实现为java.lang.ref.WeakReference。 弱引用还可以结合引用队列ReferenceQueue使用,如果弱引用指向的对象实例被回收,则JVM会将此弱引用加入到与之关联的ReferenceQueue中。

使用JVM参数 -Xms10M -Xmx10M -XX:+PrintGC 运行如下代码:

得到以下输入:

从输出中可以看到,在手动强制进行GC之后,有明显大概4M空间的回收,且我们获取到的user是null,说明再本次GC中,我们的弱引用对象被回收了。

看完了软引用和弱引用之后,可以看到这两种引用都是比较适合做那些可有可无的缓存。当系统内存资源不足时,这些缓存数据将被回收,以提供更多的内存空间。当系统内存资源充足时,这些缓存数据又可以存在相当长的时间。

应用场景: 1) ThreadLocal解决内存泄漏

先来看ThreadLocal源码中哪里用到了弱引用: ThreadLocal.ThreadLocalMap

每一个线程都会有一个ThreadLocal.ThreadLocalMap,ThreadLocalMap底层其实就是一个Entry数组,Entry的key就是ThreadLocal,value就是我们要的只能被当前线程访问的对象了。注意到这个Entry继承了WeakReference,并且此弱引用表示的类型就是我们Entry的key,也就是ThreadLocal对象,首先来假设一下如果不按照上面这么写,我们可能怎么去设计Entry?

我们是不是会这么设计,也能达到线程独享的目的,但是这样会有什么问题呢?再来理一理: 1、我们代码中使用了ThreadLocal达到线程独享数据

2、执行Test1的Thread持有一个ThreadLocal.ThreadLocalMap 3、ThreadLocal.ThreadLocalMap持有Entry 4、Entry持有ThreadLocal和Value 5、Test1对象我们使用完了,并且也被JVM回收了,意味着我们创建的这个线程独享数据不会再使用了

上述都是强引用类型,而有些线程并不是创建完就会销毁,可能伴随着我们系统同年生同月死,那就意味着可能有些ThreadLocal以及保存的Value我们后面都不会在使用了,然后因为强引用的存在,渐渐地撑爆了我们的内存,引发内存溢出。

接下来重点就是为啥使用弱引用了 弱引用在垃圾回收时,不管资源是否充足都会回收。所以如果Entry的key是一个弱引用,那么Entry的key也就是ThreadLocal将在GC时被回收掉,那么可能有人要问了,整个Entry中key是被回收掉了,但是Value依然被强引用,依然存在啊,照样存在内存泄漏啊!

是的,所以一般只要是遇到用到了ThreadLocal的时候,一定建议或者检查是否有地方对ThreadLocal进行remove方法,显示移除此Entry。

那么可能又有人要问了,既然需要我们显示remove?那还要设计key为弱引用干嘛?

我个人觉得吧,这是一个尽量解决内存泄漏的一个方案吧,因为总有粗心的程序员忘记remove对吧,或者remove永远无法被调用到等等情况,那么这个时候使用弱引用就能自动将这些不用的对象回收,并且在对ThreadLocal的get、set、remove时,如果在哈希查找的时候发现了其key是null,那么说明这个Entry失效了,此时ThreadLocal就可以保证帮我们将此Entry清理。

2) WeakHashMap 其实思想和TheadLocal一样。

3)也可以用来做缓存

五、虚引用

虚引用是四种引用类型中最弱的一种。如果一个对象实例仅持有虚引用,那么和没有引用一样,虚引用对象随时都可能被垃圾回收器进行回收,。虚引用对应的实现为java.lang.ref.PhantomReference。软引用和弱引用的使用方式比较相似,但是虚引用相比较其他引用差别就稍微大了一点:

1)通过虚引用调用get方法获取到的永远都是null,即虚应用对象永远都是不可达的,直接看源码:

2)虚引用只有一个构造方法,虚引用必须和一个引用队列ReferenceQueue结合使用

应用场景:堆外内存回收

小声哔哔:除此以外不知道这个虚引用还能有哪些场景。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/java/4969.html