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

软引用用于缓存技术。

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


相关文章
|
20天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
20天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
40 6
|
21天前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
43 4
|
2月前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
32 2
|
2月前
|
存储 Java 程序员
【一步一步了解Java系列】:何为数组,何为引用类型
【一步一步了解Java系列】:何为数组,何为引用类型
28 1
|
2月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
78 2
|
2月前
|
存储 Java 编译器
[Java]基本数据类型与引用类型赋值的底层分析
本文详细分析了Java中不同类型引用的存储方式,包括int、Integer、int[]、Integer[]等,并探讨了byte与其他类型间的转换及String的相关特性。文章通过多个示例解释了引用和对象的存储位置,以及字符串常量池的使用。此外,还对比了String和StringBuilder的性能差异,帮助读者深入理解Java内存管理机制。
21 0
|
3月前
|
算法 安全 Java
JAVA并发编程系列(12)ThreadLocal就是这么简单|建议收藏
很多人都以为TreadLocal很难很深奥,尤其被问到ThreadLocal数据结构、以及如何发生的内存泄漏问题,候选人容易谈虎色变。 日常大家用这个的很少,甚至很多近10年资深研发人员,都没有用过ThreadLocal。本文由浅入深、并且才有通俗易懂方式全面分析ThreadLocal的应用场景、数据结构、内存泄漏问题。降低大家学习啃骨头的心理压力,希望可以帮助大家彻底掌握并应用这个核心技术到工作当中。
|
3月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
3月前
|
安全 Java 数据库连接
反问面试官3个ThreadLocal的问题
接下来,我想先说说ThreadLocal的用法和使用场景,然后反问面试官3个关于ThreadLocal的话题。
反问面试官3个ThreadLocal的问题