使用 ThreadLocal 不当可能会导致内存泄露,是什么原因导致的内存泄漏呢?
我们首先看一个例子,代码如下:
/**
* Created by cong on 2018/7/14.
*/
public class ThreadLocalOutOfMemoryTest {
static class LocalVariable {
private Long[] a = new Long[1024*1024];
}
// (1)
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
// (2)
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();
public static void main(String[] args) throws InterruptedException {
// (3)
for (int i = 0; i < 50; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
// (4)
localVariable.set(new LocalVariable());
// (5)
System.out.println("use local varaible");
// localVariable.remove();
}
});
Thread.sleep(1000);
}
// (6)
System.out.println("pool execute over");
}
}
代码(1)创建了一个核心线程数和最大线程数为 6 的线程池,这个保证了线程池里面随时都有 6 个线程在运行。
代码(2)创建了一个 ThreadLocal 的变量,泛型参数为 LocalVariable,LocalVariable 内部是一个 Long 数组。
代码(3)向线程池里面放入 50 个任务。
代码(4)设置当前线程的 localVariable 变量,也就是把 new 的 LocalVariable 变量放入当前线程的 threadLocals 变量。
由于没有调用线程池的 shutdown 或者 shutdownNow 方法所以线程池里面的用户线程不会退出,进而 JVM 进程也不会退出。
运行后,我们立即打开jconsole 监控堆内存变化,如下图:
接着,让我们打开 localVariable.remove() 注释,然后在运行,观察堆内存变化如下:
从第一次运行结果可知,当主线程处于休眠时候进程占用了大概 75M 内存,打开 localVariable.remove() 注释后第二次运行则占用了大概 25M 内存,可知 没有写 localVariable.remove() 时候内存发生了泄露,下面分析下泄露的原因,如下:
第一次运行的代码,在设置线程的 localVariable 变量后没有调用localVariable.remove() 方法,导致线程池里面的 5 个线程的 threadLocals 变量里面的new LocalVariable()实例没有被释放,虽然线程池里面的任务执行完毕了,但是线程池里面的 5 个线程会一直存在直到 JVM 退出。这里需要注意的是由于 localVariable 被声明了 static,虽然线程的 ThreadLocalMap 里面是对 localVariable 的弱引用,localVariable 也不会被回收。运行结果二的代码由于线程在设置 localVariable 变量后即使调用了localVariable.remove()方法进行了清理,所以不会存在内存泄露。
接下来我们要想清楚的知道内存泄漏的根本原因,那么我们就要进入源码去看了。
我们知道ThreadLocal 只是一个工具类,具体存放变量的是在线程的 threadLocals 变量里面,threadLocals 是一个 ThreadLocalMap 类型的,我们首先一览ThreadLocalMap的类图结构,类图结构如下图:
如上图 ThreadLocalMap 内部是一个 Entry 数组, Entry 继承自 WeakReference,Entry 内部的 value 用来存放通过 ThreadLocal 的 set 方法传递的值,那么 ThreadLocal 对象本身存放到哪里了吗?
下面看看 Entry 的构造函数,如下所示:
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
接着我们再接着看Entry的父类WeakReference的构造函数super(k),如下所示:
public WeakReference(T referent) {
super(referent);
}
接着我们再看WeakReference的父类Reference的构造函数super(referent),如下所示:
Reference(T referent) {
this(referent, null);
}
接着我们再看WeakReference的父类Reference的另外一个构造函数this(referent , null),如下所示:
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}