用代码实战,彻底搞清楚ThreadLocal发生内存泄漏的情况。很多文章讲的模棱两可,在和群友的沟通中,基本弄清楚了ThreadLocal到底是什么回事,解决大多数文章都无法把知识点和实际使用结合起来讲。
先写个小例子
/**
* 测试threadLocal内存泄漏
* 01:固定6个线程,每个线程持有一个变量
* 按理来说会有 6 * 5 = 30M内存无法回收,其余的在set方法中覆盖了。
*/
public class ThreadLocalOutOfMemoryTest {
static class LocalVariable {
//总共有5M
private byte[] locla = new byte[1024 * 1024 * 5];
}
// (1)创建了一个核心线程数和最大线程数为 6 的线程池,这个保证了线程池里面随时都有 6 个线程在运行
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
// (2)创建了一个 ThreadLocal 的变量,泛型参数为 LocalVariable,LocalVariable 内部是一个 Long 数组
static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();
public static void main(String[] args) throws InterruptedException {
// (3)向线程池里面放入 50 个任务
for (int i = 0; i < 50; ++i) {
poolExecutor.execute(new Runnable() {
@Override
public void run() {
// (4) 往threadLocal变量设置值
LocalVariable localVariable = new LocalVariable();
// 会覆盖
ThreadLocalOutOfMemoryTest.localVariable.set(localVariable);
// (5) 手动清理ThreadLocal
System.out.println("thread name end:" + Thread.currentThread().getName() + ", value:"+ ThreadLocalOutOfMemoryTest.localVariable.get());
// ThreadLocalOutOfMemoryTest.localVariable.remove();
}
});
Thread.sleep(1000);
}
// (6)是否让key失效,都不影响。只要持有的线程存在,都无法回收。
//ThreadLocalOutOfMemoryTest.localVariable = null;
System.out.println("pool execute over");
}
}
复制代码
- 先打开jdk工具包下面的
jvisualvm.exe
,在运行这个小例子。等待程序结束,主动触发一次垃圾回收。
我们可以看到,大概还有30M的内存没有回收。和我们预期的结论一致。
a.为什么不是50*5=250M 呢?
因为java.lang.ThreadLocal#set()
是会覆盖的,每个线程持有一份,同一个线程执行多次,由于key是同一个ThreadLocal变量,所以会路由到数组的同一个位置上,直接覆盖上次的value。
b.为什么无法被回收掉30M的内存呢?
因为线程池中的6个线程存活,对ThreadLocalMap持有强引用
- 打开第5处的代码,再次运行观察堆内存:
这个很好理解,因为我们手动调用了remove方法,清理了ThreadLocalMap中的对象(把Entry对象指向了null),没有了强引用,当然直接被回收。
- 关闭第5处代码,再打开第6处代码:
和第一种情况一致,任然还有30M的内存无法回收。所以ThreadLocalMap的回收其实和Entry对象的Key是弱引用没有太大关系。
只要持有ThreadLocalMap的线程存活,不管Key失效或者未失效,value都不会被回收。所以才要求我们使用的过程中要即时清理。
ThreadLocalMap的整体结构
- ThreadLocalMap是ThreadLocal的内部类,内部维护了一个Entry类型的table数组。
- Entry对象由Key和Value两个成员变量,key是对ThreadLocal对象的弱引用③,Value是针对这个threadLocal存入的Object类型对象。
- key弱引用threadLocal,每次GC,如果threadLocal对象只剩下被这个ThreadLocalMap的key弱引用, 那么对象将被回收掉,key为null值。
- 程序对ThreadLocal变量的强引用,当这个强引用消失的时候,threadLocal将会被回收。类似与我们代码:
ThreadLocal local = new ThreadLocal();
local = null;
复制代码
- 线程Thread对象对整个ThreadLocalMap持有的强引用。
问题:③这里为什么要使用弱引用呢?
假设一种在Tomcat线程池使用ThradLocal的场景,线程持有对ThreadLocalMap的强引用,导致ThreadLocalMap的生命周期很长。假设为每一个请求创建一个ThreadLocal对象用于存储session,如果不及时销毁,整个ThreadLocalMap占用的内存会越来越大。 提前把Key设置为thradLocal对象的弱应用,当程序不在引用threadLocal对象的时候,gc就可以快速回收掉thradLocal对象,Entry的key为null。在ThreadLocalMap#set方法中会清理key=null的Entry对象,以达到回收内存的目的。
小结
- Thread -> ThreadLocalMap是一对一,一个线程维护一个ThreadLocalMap。
- Thread -> ThreadLocal是一对多,一个线程可以拥有多个ThreadLocal对象,分别hash映射到数组不用的索引。
- Entry中key的弱引用可以看成是协助清理ThreadLocalMap中的Entry键值对的一种方法。但是需要手动把threadLocal变量设置为null:
threadLoca=null
-
ThreadLocal内存泄漏和key采用弱引用的关系不大,因为实际编码中没人这样编码
threadLoca=null
,所以是否回收ThreadLocalMap主要取决于线程的生命周期。 - 在线程长生命周期的场景中,用完变量后,要调用ThreadLocalMap#remove()主动清理。
作者:城南码农
链接:https://juejin.cn/post/6982121384533032991
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。