【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要格外小心!


相关文章
|
2月前
|
Java
我们来说一说 ThreadLocal 内存泄漏
我是小假 期待与你的下一次相遇 ~
219 5
|
2月前
|
算法 Java
50道java集合面试题
50道 java 集合面试题
|
4月前
|
Java 数据库连接 数据库
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
本文全面总结了Java核心知识点,涵盖基础语法、面向对象、集合框架、并发编程、网络编程及主流框架如Spring生态、MyBatis等,结合JVM原理与性能优化技巧,并通过一个学生信息管理系统的实战案例,帮助你快速掌握Java开发技能,适合Java学习与面试准备。
235 2
Java 相关知识点总结含基础语法进阶技巧及面试重点知识
|
2月前
|
算法 Java
50道java基础面试题
50道java基础面试题
|
4月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
1670 0
|
4月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
486 1
|
4月前
|
存储 弹性计算 固态存储
阿里云服务器配置费用整理,支持一万人CPU内存、公网带宽和存储IO性能全解析
要支撑1万人在线流量,需选择阿里云企业级ECS服务器,如通用型g系列、高主频型hf系列或通用算力型u1实例,配置如16核64G及以上,搭配高带宽与SSD/ESSD云盘,费用约数千元每月。
451 0
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
917 0

热门文章

最新文章