如果没有看过ThreadLocal源码的,建议先去看看源码在来看这篇帖子,博主这篇帖子主要是先真实的模拟一下,什么情况下会导致内存泄漏。网上关于ThreadLocal的文档很多,就不过多追溯基础原理了。
每个Thread本身会维护一个threadLocals,这个对象就是一个ThreadLocal.ThreadLocalMap,而ThreadLocal维护一个ThreadLocalMap,这个Map的Key是一个ThreadLocal对象,value就是我们要存放的值。那在什么情况下ThreadLocal会发生内存泄漏呢,那就是在线程本身的生命周期很长,比如线程池(因为线程池创建的线程不会回收),下面是一个内存泄漏例子
jvm参数调整成这样的便于快速触发内存不足
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -verbose:gc -Xmx50m
public class Test {
public static class TestThread extends Thread {
@Override
public void run() {
ThreadLocal threadLocal = new ThreadLocal();
int[] i = new int[1024 * 1024];
threadLocal.set(i);
/**
* 如果去掉下面的注释就可以正常运行
* 因为remove内部会将ThreadLocalMap中key为null的value清除
* 这也就解释了为什么key是用弱引用修饰了,当key(ThreadLocal)被废弃的时候,可以回收掉当前ThreadLocalMap的Key
*/
//threadLocal.remove();
}
}
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor t = new ThreadPoolExecutor(4, 4, 0, TimeUnit.SECONDS, new ArrayBlockingQueue(1000), new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 100; i++) {
Thread.sleep(200);
t.execute(new TestThread());
}
t.shutdown();
}
}
总结: 首先线程池中的参数可以保证最多有4个线程执行,并且这4个线程不会被回收,那么接下来看TestThread中的逻辑,每次创建的线程会创建一个ThreadLocal对象,这些对象中会绑定1M的数据,如果正常的情况下,我们都会认为run方法执行完成后,这个作用域中的对象都是可以被回收的,但是ThreadLocal恰恰不会,因为他跟线程有如下的引用关系Thread->ThreadLocalMap->Entry->value,相当于run方法执行完成后,Entry中的Key如果被Gc后,就会变成NULL,但是Value是存在强引用的,这一点可以在程序启动两秒之后,打断点到Run方法中查看threadLocal.set(i)
方法,查看Entry中的对象Refrence都是NULL了。
最后如何避免呢,尽量不要在很长生命周期中用ThreadLocal,如果用的话,一定要调用remove方法。