ThreadLocal内存泄漏问题分析
什么是内存泄漏
简单来讲,内存泄漏就是JVM垃圾回收器对某个对象占据的内存在较长时间内一直没法回收,没法回收的原因并不是因为垃圾回收器有bug,而是由于对象没法判定为垃圾(但实际上该对象已经是不会被使用了)。
这里说的“较长时间”是一个相对的概念,没有固定的时间规定,内存泄漏侧重点在于对象不需要了而又没法回收。
ThreadLocal的存储原理
ThreadLocal存储在Thread的ThreadLocalMap对象里面,ThreadLocalMap名字带了Map但实际跟我们常说的Map并不一样,只是功能上和我们常说的Map相似。
通过查看ThreadLocal的get、set方法源码可以发现,我们在get、set时都是先获取到ThreadLocalMap对象,然后再进行取值或是赋值操作。
public class ThreadLocal<T> {
...
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
static class ThreadLocalMap {
//这里可以看出是没有实现Map接口的,和我们常用的HashMap不一样,ThreadLocalMap是ThreadLocal的一个内部类
...
}
...
}
public class Thread implements Runnable {
...
//每个线程实例都有一个threadLocals属性
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
ThreadLocalMap.Entry
ThreadLocalMap使用一个Entry数组来存放数据,Entry为ThreadLocalMap的内部类,虽然和Map.Entry同名,但也不是同一个类。ThreadLocalMap.Entry继承了弱引用类WeakReference,使用一个ThreadLocal变量作为弱引用的referent(也就是很多人常说的key),另外还有一个属性value。
public class ThreadLocal<T> {
...
static class ThreadLocalMap {
...
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
//继承WeakReference必须有一个带参的构造方法,当然参数不用跟WeakReference类完全一样
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}
...
}
ThreadLocal工作流程分析
一般我们会在某个对象里面定义一个ThreadLocal的成员变量,这样就能在这个对象的所有方法里面使用到了。假如定义为某个方法的局部变量,那可使用范围就比较窄了。
@Service
public class ThreadLocalServiceImpl implements IThreadLocalService {
private ThreadLocal<String> localVariable = new ThreadLocal<>();
@Override
public String getVariable() {
localVariable.set("1111");
...
//中间省略了一些复杂的业务方法调用,可能业务方法里面本身也有对localVariable进行了处理
System.out.println(localVariable.get());
return localVariable.get();
}
}
1.当我们定义了一个localVariable变量时,ThreadLocalServiceImpl就对localVariable有了一个强引用关系。此时还未与ThreadLocalMap产生任何关系;
2.紧接着,我们调用localVariable的set方法,此时当前线程的ThreadLocalMap就会新生成一个Entry对象,key(或者说referent)就是localVariable对象,value为我们set的值,Entry对localVariable是一个弱引用关系,但是Entry的value是强引用;
3.再然后我们打印了set的值,然后返回了,service方法执行结束。
在上述service方法执行结束之后,ThreadLocalServiceImpl对localVariable的强引用就终止了,但是当前线程没有结束,这里假设到了另外一个service里,所以Entry对localVariable的弱引用还是存在的,由于是弱引用,触发GC时localVariable就会被回收掉,此时Entry的状态就是key为null,value还有值,value无法被使用也无法被回收,内存泄漏产生。
为什么使用弱引用
假设一个不懂ThreadLocal原理的人来按上述示例代码实现功能时,如果没有弱引用(意思是假设Entry对localVariable是一个强引用关系),那最终的结果就是不仅value没被回收,key也不会被回收。因为直观感觉是localVariable为ThreadLocalServiceImpl成员变量,ThreadLocalServiceImpl的方法都执行完了,这个变量应该也被回收了。
所以弱引用是一定程度上帮助我们减轻了内存泄漏问题。另外ThreadLocal的get、set、remove方法本身有清除掉key为null的Entry的功能,所以从设计上来说也是合理的:在不出现问题情况下短时间、少量的内存溢出是合理的。
怎么避免value内存泄漏的问题
使用完之后要及时调用remove方法,这样可以直接移除掉key并触发清除key为null的Entry的功能。
@Service
public class ThreadLocalServiceImpl implements IThreadLocalService {
private ThreadLocal<String> localVariable = new ThreadLocal<>();
@Override
public String getVariable() {
localVariable.set("1111");
...
//中间省略了一些复杂的业务方法调用,可能业务方法里面本身也有对localVariable进行了处理
System.out.println(localVariable.get());
String res = localVariable.get();
localVariable.remove();
return res;
}
}
总结
ThreadLocal内存泄漏的根本原因是ThreadLocalMap存在于线程,生命周期比较长,弱引用是用来减轻内存泄漏问题的,并不是设计缺陷。