ThreadLocal内存泄漏的真正原因
Posted 小猪快跑22
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal内存泄漏的真正原因相关的知识,希望对你有一定的参考价值。
注意:
看这篇文章之前得对 ThreadLocal 有个大致的了解,不然看起来还是蛮吃力的。
一、内存泄漏是因为弱引用吗?
先说结果,内存泄漏的确是因为弱引用引起的,为什么呢?
先看下 ThreadLocal
的 set
方法:
public void set(T value)
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
createMap 如下:
void createMap(Thread t, T firstValue)
// t 指的是当前线程,即调用 threadLocal.set方法所在的线程
t.threadLocals = new ThreadLocalMap(this, firstValue);
可以看出ThreadLocal
的数据是存在 ThreadLocalMap
,而 ThreadLocalMap 是线程Thread
中的变量,所以可以做到线程的数据隔离。
ThreadLocalMap 中是以数组的形式存放数据 private Entry[] table
,至于插入数据时产生的碰撞这里不作说明了,可能下一篇会说明。Entry 的数据结构如下:
static class Entry extends WeakReference<ThreadLocal<?>>
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v)
super(k); // key 为 ThreadLocal的弱引用
value = v;
我们把上面的引用链画下来,如下:
其中,虚线表示弱引用。
下面来说下为什么弱引用会导致内存泄漏?
一般项目中使用线程都是利用线程池,而线程池中的线程可能一直存活,这就导致了当外部不再持有 ThreadLocal
的引用的时候,发生GC,导致 ThreadLocal
对象被回收了。
当 Entry 中的 key 即 ThreadLocal
对象被回收了之后,会发生 Entry 中 key 为 null
的情况,其实这个 Entry 就已经没用了,但是又无法被回收,因为有 Thread->ThreadLocalMap ->Entry
这条强引用在,这样没用的内存无法被回收就是内存泄露。
当然,如果不是线程池使用方式的话,其实不用关系内存泄漏,反正线程执行完了就都回收了,但是一般我们都是使用线程池。
但是,没关系,在作者设计 ThreadLocal 的时候已经考虑到这种情况了,所以在 ThreadLocal
的 set
、get
、以及扩容的时候都会清理 key = null 的数据。但是最佳实践是当不再使用的时候,手动 remove 掉,如下:
void dosth
threadlocal.set("1234");
try
// do sth ...
finally
threadlocal.remove();
既然弱引用会导致内存泄漏,那为什么不用强引用呢?
同样的,如果是强引用,如果线程一直在,那么就算外部不再持有 ThreadLocal 的引用,但是会存在如下的引用链 GCRoots -> 线程对象 -> ThreadLocalMap -> Entry -> Key(ThreadLocal)
,导致ThreadLocal
对象得不到回收。
看到这里,可能有人会说那线程被回收之后就好了呀。
重点来了!线程在我们应用中,常常是以线程池的方式来使用的,而线程池中的线程一般是不会被清理掉的,所以这个引用链就会一直在,那么 ThreadLocal
对象即使没有用了,也会随着线程的存在,而一直存在着!
可以通过如下的方法来看看弱引用的 ThreadLocal 是否被回收。
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("我是在主线程中设置的值");
// 如果不设置为null,那么由于有强引用持有 threadLocal,那么不会被回收
threadLocal = null; // 断开 ThreadLocal 的强引用
System.gc(); // 主动垃圾回收
Thread curThread = Thread.currentThread();
Class<? extends Thread> clz = curThread.getClass();
Field field = null;
try
field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(curThread);
Class<?> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr)
if (o == null) continue;
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s 值:%s", referenceField.get(o), valueField.get(o)) + " , thread >> " +Thread.currentThread().getName());
catch (NoSuchFieldException | IllegalAccessException e)
e.printStackTrace();
以上是关于ThreadLocal内存泄漏的真正原因的主要内容,如果未能解决你的问题,请参考以下文章