# ThreadLocal 内存溢出代码演示

下面我们用代码实现 ThreadLocal 内存溢出的情况,请参考以下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ThreadLocalTest {
static ThreadLocal threadLocal = new ThreadLocal();
static Integer MOCK_MAX = 10000;
static Integer THREAD_MAX = 100;
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_MAX);
for (int i = 0; i < THREAD_MAX; i++) {
executorService.execute(() -> {
threadLocal.set(new ThreadLocalTest().getList());
System.out.println(Thread.currentThread().getName());
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
executorService.shutdown();
}
List getList() {
List list = new ArrayList();
for (int i = 0; i < MOCK_MAX; i++) {
list.add("Version:JDK 8");
list.add("ThreadLocal");
list.add("Author:老王");
list.add("DateTime:" + LocalDateTime.now());
list.add("Test:ThreadLocal OOM");
}
return list;
}
}

设置 JVM(Java 虚拟机) 启动参数 -Xmx100m (最大运行内存 100 M ),运行程序不久后就会出现如下异常:

此时我们用 VisualVM 观察到程序运行的内存使用情况,发现内存一直在缓慢地上升直到内存超出最大值,从而发生内存溢出的情况。

内存使用情况,如下图所示:

# 内存溢出原理分析

在开始之前,先来看下 ThreadLocal 是如何存储数据的。
首先,找到 ThreadLocal.set() 的源码,代码如下:

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

可以看出 ThreadLocal 首先获取到 ThreadLocalMap 对象,然后再执行 ThreadLocalMap.set() 方法,进而打开此方法的源码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

从整个代码可以看出,首先 ThreadLocal 并不存储数据,而是依靠 ThreadLocalMap 来存储数据, ThreadLocalMap 中有一个
Entry 数组,每个 Entry 对象是以 K/V 的形式对数据进行存储的,其中 K 就是 ThreadLocal 本身,而 V
就是要存储的值,如下图所示:

可以看出:一个 Thread 中只有一个 ThreadLocalMap ,每个 ThreadLocalMap 中存有多个 ThreadLocalThreadLocal 引用关系如下:

enter image descriptionhere

其中:实线代表强引用,虚线代表弱引用(弱引用具有更短暂的生命周期,在执行垃圾回收时,一旦发现只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存)。

看到这里我们就理解了 ThreadLocal 造成内存溢出的原因:如果 ThreadLocal 没有被直接引用(外部强引用),在 GC(垃圾回收)时,由于
ThreadLocalMap 中的 key 是弱引用,所以一定就会被回收,这样一来 ThreadLocalMap 中就会出现 keynull
Entry,并且没有办法访问这些数据,如果当前线程再迟迟不结束的话,这些 keynullEntryvalue 就会一直存在一条强引用链: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 并且永远无法回收,从而造成内存泄漏。

# 解决

切记 用完之后就 remove 掉!

# 最后

希望和你一起遇见更好的自己。