【Java面试】说说你对ThreadLocal内存泄漏问题的理解

简介: 【Java面试】说说你对ThreadLocal内存泄漏问题的理解

前置知识

讲解ThreadLocal的内存泄漏问题之前,首先得先知道什么是内存泄漏。

Memory overflow:内存溢出没有足够的内存提供申请者使用。

Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

一文讲清强软弱虚四种引用类型

ThreadLocal的内存泄露问题是怎么导致的?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中

为什么会产生内存泄漏问题?

下面是大致的结构图,在栈中存放当前线程对象的引用以及ThreadLocal的引用。

他们指向了堆中的对象实例。

我们知道ThreadLocalMap(下文统称为Map)中的Entry的key使用的是ThreadLocal对象。

那么如果这里是一个强引用,那么如果我们设置ThreadLocal的引用为null,那么此时由于Map中的的Entry强引用了ThreadLocal作为键,因此此时会造成ThreadLocal无法被回收,在没有手动删除这个Entry或者当前线程仍然在运行的情况下,始终有强引用链 Thread的引用-》Thread对象-》Map-》Entry-》Key(ThreadLocal)和value-》内存。 此时就造成了内存泄漏。

因此如果ThreadLocalMap中的key使用了强引用,是完全无法避免内存泄漏的。

那么我们知道,ThreadLocalMap中的Entry继承了一个弱引用。也就是情况如下:

和上面不同的地方在于,在ThreadLocal的引用被回收之后,没有任何一个强引用指向ThreadLocal对象了,那么此时ThreadLocal就会被gc回收。因此此时Entry中的key为null。

但是,在没有手动删除这个Entry以及当前线程依旧运行的情况下,还是存在强引用链

Thread的引用-》Thread对象-》Map-》Entry-》Key(null)和value-》内存,value依旧不会被回收,而且这块value永远不会被访问到了,导致内存泄漏。

也就是说即使使用了弱引用,还是会导致内存泄漏。

如何解决内存泄露问题?

你可能很疑惑,都已经使用了弱引用了,为什么还是内存泄漏啊?

因为弱引用本身就不是为了解决这个问题而使用的。那么内存泄漏的真正原因是说明?

其实无非两个原因:

  • Entry没有被删除
  • 外部线程依旧在运行

第一点好理解,只要在使用完毕ThreadLocal之后调用remove方法删除Entry就可以避免内存泄漏。

第二点比较复杂,由于ThreadLocalMap是Thread的一个属性,并且被当前线程所引用,他的生命周期和Thread一样长,那么在使用完毕ThreadLocal的时候,Thread也随之结束,那么ThreadLocalMap自然也会被gc回收,也就从根源上避免了内存泄漏问题。

综上,内存泄露问题的根源是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应数据,而只是把ThreadLocal设定为空,就会导致内存泄漏。

为什么要使用弱引用?

根据刚才的分析我们知道了:无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

要避免内存泄漏有两种方式:

  • 使用完ThreadLocal,调用其remove方法删除对应的Entry
  • 使用完ThreadLocal,当前Thread也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。

那么为什么key要用弱引用呢?

事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null (也即是ThreadLocal为null )进行判断,如果为null的话,那么是会对value置为null的。

这就意味着使用完ThreadLocal , 当前线程依然运行的前提下,就算忘记调用remove方法,弱引用比强引可以多一层保障∶弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

ThreadLocal可能引起的OOM内存溢出问题简要分析

我们知道ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会

一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMap,Thread调用

exit方法如下:

private void exit() {
        if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
            TerminatingThreadLocal.threadTerminated();
        }
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

但是,当我们使用线程池的时候,就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存

在的)。如果这样的话,将一些很大的对象设置到ThreadLocal中(这个很大的对象实际保存在Thread

的threadLocals属性中),这样的话就可能会出现内存溢出的情况。

一种场景就是说如果使用了线程池并且设置了固定的线程,处理一次业务的时候存放到

ThreadLocalMap中一个大对象,处理另一个业务的时候,又一个线程存放到ThreadLocalMap中一个大

对象,但是这个线程由于是线程池创建的他会一直存在,不会被销毁,这样的话,以前执行业务的时候

存放到ThreadLocalMap中的对象可能不会被再次使用,但是由于线程不会被关闭,因此无法释放Thread 中的ThreadLocalMap对象,造成内存溢出。也就是说,ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。所以我们

在使用线程池的时候,使用ThreadLocal要格外小心!


相关文章
|
11天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
14天前
|
算法 Java Go
Go vs Java:内存管理与垃圾回收机制对比
对比了Go和Java的内存管理与垃圾回收机制。Java依赖JVM自动管理内存,使用堆栈内存并采用多种垃圾回收算法,如标记-清除和分代收集。Go则提供更多的手动控制,内存分配与释放由分配器和垃圾回收器协同完成,使用三色标记算法并发回收。示例展示了Java中对象自动创建和销毁,而Go中开发者需注意内存泄漏。选择语言应根据项目需求和技术栈来决定。
|
2天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
12 0
|
2天前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
6 0
|
3天前
|
存储 算法 Java
了解Java内存管理与垃圾回收机制
了解Java内存管理与垃圾回收机制
6 0
|
3天前
|
存储 Java
面试高频 ThreadLocal类详解
面试高频 ThreadLocal类详解
6 0
|
4天前
|
Java
三个可能的Java面试题
Java垃圾回收机制自动管理内存,回收无引用对象的内存,确保内存有效利用。多态性允许父类引用操作不同子类对象,如Animal引用可调用Dog的方法。异常处理机制通过try-catch块捕获和处理程序异常,例如尝试执行可能导致ArithmeticException的代码,catch块则负责处理异常。
25 9
|
12天前
|
存储 机器学习/深度学习 Java
【Java探索之旅】数组使用 初探JVM内存布局
【Java探索之旅】数组使用 初探JVM内存布局
26 0
|
15天前
|
Java
【JAVA面试题】static的作用是什么?详细介绍
【JAVA面试题】static的作用是什么?详细介绍
|
15天前
|
Linux
Linux rsyslog占用内存CPU过高解决办法
该文档描述了`rsyslog`占用内存过高的问题及其解决方案。
40 4