ThreadLocal 内存泄露分析

ThreadLocal 用于在线程中传递数据,在使用之后一定要记得调用 remove 方法,否则有可能造成内存泄露,下面从源码来理解下原因。

从 ThreadLocal 的 get() 入手,get 要从一个 map 中拿数据,如果能拿到就返回值,否则创建 map,map 的 key 就是 ThreadLocal 对象,value 就是设置的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// ① 从当前线程获取 map
ThreadLocalMap map = getMap(t);
if (map != null) {
// map 不为空,就将 ThreadLocal 对象作为 key 在 map 中查询 Entry 对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// entry 不为空,就返回值,也就是 ThreadLocal 对象调用 get 方法的值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// ② map 为空或者 entry 为空,则初始化值
return setInitialValue();
}

代码片段1

再看代码片段1的代码①的 getMap(t),获取的 map 是和 Thread 对象绑定的,也就是说每个 Thread 对象拥有不同的 map,这样不同线程保存到 map 中的数据就互相隔离开了。

1
2
3
4
ThreadLocalMap getMap(Thread t) {
// 从 Thread 对象中拿到 threadLocals,该对象是 ThreadLocal 类中实现的类似 Map 的数据结构类
return t.threadLocals;
}

代码片段2

threadLocals 对象的类型是 ThreadLocal$ThreadLocalMap,是 ThreadLocal 的一个内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class ThreadLocalMap {

// 主要数据结构类似 Map 的 Entry,用于保存 key 和 value,多个 Entry 在 Map 中通过数组存储
static class Entry extends WeakReference<ThreadLocal<?>> {

Object value;

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

// 其他代码省略
}

代码片段3

再看代码片段1的代码②的 setInitialValue(),创建 map 的代码,再次验证 map 是否为 null,是则创建,否则直接设值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private T setInitialValue() {
// 初始化 value,就是 null
T value = initialValue();
// 获取当前线程,用于获取线程绑定的 map
Thread t = Thread.currentThread();
// 获取线程的 map,同代码片段2
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果 map 不为 null,设置当前 key 就是 ThreadLocal 对象,值为 null
map.set(this, value);
else
// 否则创建 map
createMap(t, value);
return value;
}

// 创建 map 代码,就是创建了一个 ThreadLocalMap 对象,类代码同代码片段3
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

代码片段4

从 get 方法就可以知道 ThreadLocal 的数据结构如下图1,其中 ttl 分别是 Thread 对象和 ThreadLocal 对象的引用,thread 框表示 Thread 对象内部结构,内部有个 map 是 threadmaps,map 中是 entry 数组,每个 entry 中包含 keyvalue,key 指向的就是 ThreadLocal 对象,和 lt 指向的是同一个对象,value 指向的是值对象。

可以看到 key 指向的 ThreadLocal 对象的箭头是虚线,这表示是一个弱引用,可以通过代码片段3看到,Entry 类继承自 WeakReference<ThreadLocal<?>>,构造函数调用了 super(k),那么 key 指向对象的引用就是一个弱引用,即当 key 指向的对象只有 key 到对象的引用时,key 指向的对象将被 GC 回收。

ThreadLocal 数据结构清楚了,再来分析下为什么一定要调用 remove 方法,先看看源码。

1
2
3
4
5
6
7
public void remove() {
// 获取线程的 map
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// map 不为 null,则通过 ThreadLocal 对象移除值
m.remove(this);
}

代码片段5

假设 ttl 是在方法中创建的,也就是方法执行完毕后,tlt 会被清除,这样 threadthreadlocal 的引用被清楚了,它们也会被清除,然后 threadmpas 的引也耶没有了,也会被清除,然后 entry 的引用也没有了,entry 也会被清除,然后 object 的引用也没有了,也会被清除,这样所有对象都被清楚了,即使不调用 remove 方法也 不会造成内存泄露

但是一般情况下 thread 是通过线程池创建,也就是说 t 不会被清除,那么 thread 不会清除,threadmaps 也不会被清除,entry 也不会被清除,keyvalue 也不会被清除,其指向的 threadlocalobject 也不会被清除,那么如果不调用 remove 方法,线程中的 map 指向的 threadlocalobject 对象会越来越多,并且无法清除,就造成了内存泄露

ThreadLocal 在设计的时候将 key 设计为弱引用,也是为了尽可能的防止内存泄露,当 lt 被清除后,threadlocal 就只有 key 指向了,由于是弱引用,threadlocal 会被清除,那么 key 就指向 null 了,如下图2所示。

ThreadLocal 在 set 和 get 的时候会去检查是否有 key 为 null,是则自动 remove,这样就做到尽最大可能放置内存溢出,源码查看 expungeStaleEntry(int staleSlot)

但是自动 remove 就有个前提条件了,必须要没有执行 remove 的线程执行 ThreadLocal 对象的 get 后者 set 方法才能触发,如果一直不执行呢,比如该线程一直在线程池里面没有被分配,或者分配了,又没有调用 get 或者 set,最后就算调用了,但是 get 和 set 清理 key 为 null 的 entry 时也是有条件的,并不是每次都要遍历所有的 entry,也就是说,不执行 remove,就有可能出现内存泄露

其实,我写代码一般 lt 都设置为常量,这样连 threadlocal 都无法自动清除,更不可能清除 object 了,所以 remove 方法一定要执行,并且在 finally 中执行。

最后,再总结下分析结果:

  1. ThreadLocal 不执行 remove 方法可能造成内存泄露。
  2. 内存泄露发生的条件是 Thread 对象不被清除时,这种情况一般就是在使用线程池的情况下。
  3. ThreadLocal 通过对弱引用只是尽可能的防止内存泄露,并且还需要触发条件才能自动 remove。
  4. 发生内存泄露的对象是 ThreadLocal 的值对象,即图1中的 object。
  5. ThreadLocal 使用后一定要在 finally 中执行 remove 方法。