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

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

四种引用类型

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中用于防止内存泄漏。

软引用用于缓存技术。

虚引用用于通知垃圾回收器回收直接内存中的数据。


相关文章
|
6天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
11天前
|
存储 缓存 Oracle
Java I/O流面试之道
NIO的出现在于提高IO的速度,它相比传统的输入/输出流速度更快。NIO通过管道Channel和缓冲器Buffer来处理数据,可以把管道当成一个矿藏,缓冲器就是矿藏里的卡车。程序通过管道里的缓冲器进行数据交互,而不直接处理数据。程序要么从缓冲器获取数据,要么输入数据到缓冲器。
Java I/O流面试之道
|
1天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
9 2
|
6天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
21 6
|
7天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
26 4
|
8天前
|
存储 Java 程序员
Java基础的灵魂——Object类方法详解(社招面试不踩坑)
本文介绍了Java中`Object`类的几个重要方法,包括`toString`、`equals`、`hashCode`、`finalize`、`clone`、`getClass`、`notify`和`wait`。这些方法是面试中的常考点,掌握它们有助于理解Java对象的行为和实现多线程编程。作者通过具体示例和应用场景,详细解析了每个方法的作用和重写技巧,帮助读者更好地应对面试和技术开发。
41 4
|
19天前
|
存储 Java
[Java]面试官:你对异常处理了解多少,例如,finally中可以有return吗?
本文介绍了Java中`try...catch...finally`语句的使用细节及返回值问题,并探讨了JDK1.7引入的`try...with...resources`新特性,强调了异常处理机制及资源自动关闭的优势。
18 1
|
18天前
|
算法 Java
JAVA 二叉树面试题
JAVA 二叉树面试题
14 0
|
存储 Java
java中ThreadLocal的使用
java中ThreadLocal的使用
|
Java
Java ThreadLocal使用
ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。
836 0