【Java面试】Java中都有那些引用类型?(关于弱引用是如何解决ThreadLocal内存泄漏问题)

60 篇文章 1 订阅

四种引用类型

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中用于防止内存泄漏。
软引用用于缓存技术。
虚引用用于通知垃圾回收器回收直接内存中的数据。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZhangBlossom

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值