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

软引用用于缓存技术。

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


相关文章
|
23天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
60 2
|
6天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
14 0
|
11天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
35 14
|
8天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
21 8
|
14天前
|
Java
java内存区域
1)栈内存:保存所有的对象名称 2)堆内存:保存每个对象的具体属性 3)全局数据区:保存static类型的属性 4)全局代码区:保存所有的方法定义
20 1
|
5天前
|
存储 监控 算法
Java内存管理的艺术:深入理解垃圾回收机制####
本文将引领读者探索Java虚拟机(JVM)中垃圾回收的奥秘,解析其背后的算法原理,通过实例揭示调优策略,旨在提升Java开发者对内存管理能力的认知,优化应用程序性能。 ####
19 0
|
4月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
399 0
|
2月前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
80 1
|
2月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
2月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。