每日7:30准时推送干货:ThreadLocal内存泄露原因及避免方法
每天早上七点三十,准时推送干货

说到内存溢出,大家都清楚是什么。然而说到内存泄露,尤其是 ThreadLocal ,那阿粉就得来讲讲了。毕竟在面试时如果被问到 ThreadLocal 的内存泄露,或许就可能不太了解了。今天阿粉来讲讲 ThreadLocal 内存泄露的原因,以及怎样在开发中避免这个问题。
什么是内存泄露
提到内存泄露,阿粉得来讲一下。对于初中级程序员而言,这个可能较为陌生。之所以这么说,是因为 JVM 有自身的内存回收机制,所以初中级程序员很少有接触到内存泄露的情况。而内存泄露的意思是,程序在申请内存后,无法释放已申请的内存空间。一次内存泄露的危害可能可以忽略,但内存泄露不断堆积,后果就会很严重,无论内存有多少,迟早都会被占光。
我们知道,有时候在定义变量时,就应明白它需要一段内存空间来存储数据信息。若这段内存一直不被释放,就会致使内存被占用光。而被占用的这个对象一直不能被回收掉,这就是内存泄漏。
在谈及 ThreadLocal 的内存泄露之前,我们首先来讲讲 ThreadLocal 的实现原理。接着,我们再来分析它出现泄露的原因究竟是什么。
创建一个 ThreadLocalMap,它的父映射为传入的 parentMap。这个 ThreadLocalMap 是私有的。
Entry 的 parentTable 是 parentMap 的 table 。
定义一个整数变量 len,它的值等于 parentTable 的长度。
设置阈值(len)。
创建了一个名为 table 的数组,其类型为 Entry 数组,数组的长度为 len,即通过 new 关键字为该数组分配了内存空间。
for (int j = 0; j < len; j++) {
Entry e 等于 parentTable 中的第 j 个元素。
if (e != null) {
@SuppressWarnings 这个注解用于抑制特定类型的编译器警告。它可以应用于方法、类、字段等代码元素上。通过使用 @SuppressWarnings 注解,开发人员可以告诉编译器忽略某些警告,这些警告可能是由于代码的特定特性或使用的库引起的,但并不一定表示存在实际的错误。开发人员应该谨慎使用 @SuppressWarnings 注解,只有在确实知道如何处理警告或确定警告不会影响代码的正确性和安全性时才使用它。在使用 @SuppressWarnings 注解时,应该明确指定要抑制的警告类型,以避免抑制其他可能重要的警告。"unchecked")
ThreadLocal 是一种用于在多线程环境下存储和管理线程局部变量的机制。它可以为每个线程提供独立的变量副本,避免了线程安全问题。通过使用 ThreadLocal,开发人员可以在不使用同步机制的情况下,安全地在多个线程之间共享数据。每个线程都可以独立地修改自己的变量副本,而不会影响其他线程的变量副本。ThreadLocal 通常用于存储与线程相关的数据,例如用户身份验证信息、事务上下文等。在使用 ThreadLocal 时,需要注意线程的生命周期和变量的销毁时机,以避免内存泄漏等问题。
每一个 ThreadLocal 都有一个与之对应的 ThreadLocalMap 。这个 ThreadLocalMap 中,key 是使用了弱引用的 ThreadLocal 实例,而 value 则是线程变量的副本。
那么什么又是弱引用呢?
引用的话分为两类,有弱引用就必然有强引用,所以我们要区分强引用和弱引用。
强引用
引用中最普遍的一种是,若一个对象有强引用,那么它不会被垃圾回收器回收。当内存空间不够时,Java 虚拟机宁可抛出 OutOfMemoryError 错误,让程序异常终止,也不会回收这类对象。
如果想要取消强引用与某个对象的关联,那么可以将引用明确地赋值为 null ,如此一来,JVM 就能在恰当的时间回收该对象。
弱引用
JVM 进行垃圾回收的时候,不管内存是不是充足,都会对被弱引用关联的对象进行回收。在 java 领域里,是用 java.lang.ref.WeakReference 类来进行表示的。并且可以在缓存当中使用弱引用。
阿粉曾说,每一个 ThreadLocal 会维护一个 ThreadLocalMap。这个 ThreadLocalMap 中的 key 是使用了弱引用的 ThreadLocal 实例,value 则是线程变量的副本。他们之间的引用关系如下图:
在这个图中,
实线表示-强引用
虚线表示-弱引用
从图中可以分析出这个 ThreadLocal 出现内存泄露的方式。我们能看到,ThreadLocalMap 以 ThreadLocal 的弱引用作为 key。若一个 ThreadLocal 不存在外部强引用,那么 Key(ThreadLocal)必然会被 GC 回收,这会致使 ThreadLocalMap 中的 key 为 null,而 value 仍存在强引用。只有 Thread 线程退出后,value 的强引用链条才会断开。
如果当前线程迟迟不结束,那么这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链。
Thread Ref 指向 Thread ,Thread 又指向 ThreaLocalMap ,ThreaLocalMap 指向 Entry ,Entry 指向 value 。
在这个时候,如果无法进行回收,就会引发 ThreadLocal 出现内存泄露的状况。
这个时候会有读者提出疑问,为何 ThreaLocalMap 使用 ThreadLocal 的弱引用,而不使用强引用呢?若使用强引用,是否就不会出现这个内存泄露的问题呢?
其实这完全是两码事。因为当此时 ThreadLocalMap 的 key 为强引用且要回收 ThreadLocal 时,由于 ThreadLocalMap 持有 ThreadLocal 的强引用,若未手动删除,ThreadLocal 就不会被回收,进而会导致 Entry 出现内存泄漏。
这是使用强引用的时候,那么使用弱引用的时候是什么样的呢?
ThreadLocalMap 的 key 为弱引用时会回收 ThreadLocal 。因为 ThreadLocalMap 持有 ThreadLocal 的弱引用,所以即便没有手动删除,ThreadLocal 也会被回收。当 key 为 null 时,在接下来的 ThreadLocalMap 调用 set()方法时,会清除 value 值;在调用 get()方法时,会清除 value 值;在调用 remove()方法时,也会清除 value 值。
所以这么看下来,反而使用弱引用,却是更好的为什么呢?
使用弱引用能多有一层保障。弱引用的 ThreadLocal 不会出现内存泄漏的情况,并且在下次 ThreadLocalMap 调用 set()、get()或 remove()方法时,对应的 value 会被清除。
因此,ThreadLocal 内存泄漏有其根本原因。ThreadLocalMap 的生命周期与 Thread 相同。如果没有手动删除对应 key,就会引发内存泄漏,而非是因为弱引用。
我们接着来查看这个 ThreadLocal 所维护的 ThreadLocalMap 的源码分析。
static class ThreadLocalMap {
//hreadLocalMap 中的数据存储在 Entry 类型数组的 table 里,Entry 继承了 WeakReference(弱引用)
静态类 Entry 是 WeakReference 的子类。> {
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
**成员变量**
//初始容量
私有静态最终整型 INITIAL_CAPACITY 的值为 16 。
//ThreadLocalMap数据真正存储在table中
private Entry[] table;
//ThreadLocalMap条数
private int size = 0;
//达到这个大小,则扩容
private int threshold;
构造函数
ThreadLocalMap(ThreadLocal>对于 (firstKey, Object firstValue) 来说:
初始化 table 数组,将 INITIAL_CAPACITY 的默认值设定为 16。
创建了一个名为 table 的数组,其类型为 Entry 数组,数组的初始容量为 INITIAL_CAPACITY ,并将其赋值给 table 。
//key和16取得哈希值
定义一个整数 i ,它等于 firstKey 的 threadLocalHashCode 与 (INITIAL_CAPACITY - 1) 进行按位与运算的结果。
//创建节点,设置key-value
创建一个新的 Entry 对象,其键为 firstKey,值为 firstValue,然后将该对象赋值给 table[i]。
size = 1;
//设置扩容阈值
设置阈值(初始容量)。
}
我们再来看看他的 remove 方法
在这个方法中,需要移除指定的 ThreadLocal 对象。具体来说,就是要找到与之相关联的线程,并从该线程的 ThreadLocal 映射中删除对应的键值对。这样做可以确保在后续的操作中,不会再访问到已经被移除的 ThreadLocal 数据,从而避免潜在的内存泄漏问题。> key) {
Entry[] tab = table;
int len = tab.length;
定义一个整数 i ,让其等于 key 的 threadLocalHashCode 与 (len - 1) 进行按位与运算。
如果 threadLocalHashCode 计算出的下标所找到的 key 与传入的 key 不一样,那就证明出现了哈希冲突,接着就循环向下进行查找。
for (Entry e = tab[i];
e != null;
在 i 等于 nextIndex(i, len)时,e 等于 tab[i]。
//如果key相同
if (e.get() == key) {
//删除当前Entry
e.clear();
//清理
删除过期条目(i)。
return;
}
}
}
ThreadLocal 中的 ThreadLocalMap 包含 set 方法和 getEntry 方法等,还有许多其他内容,阿粉就不再详细介绍了。
那么我们应该怎样合理地使用 ThreadLocal 呢?这样才能保证内存不会泄露。
号外!号外!
Java 极客技术微信群里有许多优秀的小伙伴在探讨技术。偶尔会有不定期的资料分享活动,也会发放红包。如果你希望提升自己,还想与优秀的人一同进步,感兴趣的朋友,可以在下方公众号的后台回复:加群。