java的threadlocal为何内存泄漏

ThreadLocal 的实现原理:每一个 Thread 维护一个 ThreadLocalMap,key 为使用弱引用的 ThreadLocal 实例,value 为线程变量的副本,这些对象之间的引用关系如下:

实心箭头表示强引用,虚心箭头表示弱引用

ThreadLocal 的内存泄露发生在 Entry 上,我们现在来详细分析 Entry。

对于 Entry 的 key 来说,它是 ThreadLocal 对象,它有两个引用源,一个是栈内存上的 ThreadLocal Ref,一个是 Entry 中的 key,如下:

对于 Entry 的 value 而言它就只有一条引用链:

对于 Entry 来说,由于存在 key 和 value 两个引用路径,所以这里就会有两种情况:

栈上的 ThreadLocal Ref 不再使用了,但是由于 ThreadLocal 对象还有一条引用链存在,这就会导致它无法被回收,时间久了就会导致内存泄露。

由于线程池的存在,会让线程一直被重复利用,就会导致第二条 value 链一直存在,导致 ThreadLocalMap 无法被回收,从而导致内存泄露。

内存泄露的解决方案

第一种情况:弱引用

栈上的 ThreadLocal Ref 不再使用了,但是由于 ThreadLocal 对象还有一条引用链存在,这就会导致它无法被回收,时间久了就会导致内存泄露。

为了解决这种情况,ThreadLocal 使用了弱引用。即上图用虚线表示的部分。源代码如下:

tatic class ThreadLocalMap {

复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
...

}

对 JVM 熟悉的小伙伴知道,如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。

当堆栈上的 ThreadLocal Ref 不再使用了,ThreadLocal 对象就只有弱引用了,那么 ThreadLocal 对象就可以在下次GC时被回收掉了。

第二种情况:使用完 ThreadLocal 后调用 remove()

由于线程池的存在,会让线程一直被重复利用,就会导致第二条 value 链一直存在,导致 ThreadLocalMap 无法被回收,从而导致内存泄露。

value 的生命周期与 Thread 是一样的,由于线程池的存在,导致线程一直都被重复利用,从而导致 value 对象一直都无法被释放。那怎么解决呢?

我们知道 ThreadLocal 本身其实是不存储数据的,数据都是存在 ThreadLocalMap 中的,所以我们在每次调用ThreadLocal的get()、set()、remove() 方法的时候,内部实际会调用ThreadLocalMap的get()、set()、remove() 操作。而 ThreadLocalMap 的这些方法都会清理key为null,但是value还存在的Entry。

所以,当我们在一个 ThreadLocal 用完之后,可以手动调用一下remove(),就可以在下一次GC的时候,把Entry清理掉。

总结

由于 Thread 中包含了 ThreadLocalMap 的变量,因此 ThreadLocalMap 与 Thread 的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是,由于 ThreadLocal 使用了弱引用,则多了一层保障:弱引用 ThreadLocal 不会内存泄漏,因为它会在下一次 GC 时被回收。

ThreadLocal 内存泄漏的根源是:Thread 被重复利用,导致 value 强引用链一直存在,而导致内存泄露,注意,不是因为弱引用。

所以,当我们用完一个 ThreadLocal 后,可以手动调用一下remove(),就可以在下一次GC的时候,把Entry清理掉。

为什么我们在使用 ThreadLocal 的时候,一再强调要手动调用 remove() 方法。

相关推荐
apcipot_rain16 小时前
C++转Java日记(06)
java·开发语言
金銀銅鐵21 小时前
带您看懂字节码指令(一):一帧里有什么
java·后端·ai编程
nice_lcj5201 天前
【从零理解 Spring AI · 14】RAG 的四个环节:加载、切分、向量化、检索
java·spring boot·embedding·向量数据库·rag·spring ai·ai应用开发
学长毕设1 天前
基于springboot科创竞赛信息平台-附源码
java·spring boot·后端
聪明的笨猪猪1 天前
Java 工程案例:AI 生成 @Async 异步线程池踩坑
java·经验分享·面试·ai编程
学长毕设1 天前
基于SpringBoot公务员考试交流平台-附源码
java·spring boot·后端
SL_staff1 天前
私有化低代码:面向强监管行业的可控数字化实践
java·低代码
三翼鸟数字化技术团队1 天前
高性能存储引擎RocksDB:原理与Java生态实践
java
SL_staff1 天前
JVS-APS有限产能建模的七个刚性支点:面向产线真实约束的技术实现解析
java·开源·全栈
特立独行的猫A1 天前
少 3 个环节、少 1 次 ABI、少 3500 行代码:cj-tauri 与 Ionic/Capacitor 的鸿蒙桥接机制对比
java·后端·python