提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、演示一个OOM的案例
- 二、分析原因
- 1.直接说原因
- 2.看源码找原因
- 总结
前言
提到ThreadLocal,在网上你会看到很多说它有内存泄露的问题,提到的解决方案也很简单,就是用完ThreadLocal对象后,要调用remove();
方法进行清除。那么这个问题是如何产生的呢?为什么remove就能解决呢?今天我们就来一探究竟!
一、演示一个OOM的案例
注意:修改JVM参数,设置小一点。-Xms20M -Xmx20M -Xmn10M
/**
* @author KinYang.Lau
* @date 2021/3/30 8:17 下午
* -Xms20M -Xmx20M -Xmn10M
*/
public class ThreadLocalOOM {
public static void main(String[] args) {
System.out.println("main 线程开始...");
for (int i = 0; i < 100; i++) {
ThreadLocal<Byte[]> threadLocal = new ThreadLocal<>();
/// 在当前线程存入一个value
System.out.println("在线程:"+Thread.currentThread().getName()+" 添加一个threadLocal");
threadLocal.set(new Byte[1024*1024]);
}
System.out.println("main 线程完美运行结束");
}
}
运行结果:
上面的程序就做了一件事情,就是在主线程 main 中,循环添加ThreadLocal对象。
二、分析原因
1.直接说原因
分析:
程序很简单,就是new一个对象,然后给对象set一个 Byte数组。一次for循环结束,这个对象应该就可以被GC回收,所以这个循环不论多少次,都不会造成OOM。但是这里为什么会出现OOM呢?肯定是ThreadLocal对象有问题,难道一个for循环结束后,ThreadLocal 的对象不会被GC回收吗?
答案是:ThreadLocal对象会回收。但是导致OOM的不是ThreadLocal对象没有被及时回收,而是new的Byte数组,没有GC被回收。
2.看源码找原因
为什么我上面说,ThreadLocal对象被回收了,但是Byte数组没有被回收,那么这个Byte数组是被谁引用着而导致没有被回收呢?我先说答案:是当前的线程所引用,这里也就是main线程。
来看ThreadLocal的set方法:
public void set(T value) {
/// 获取当前线程
Thread t = Thread.currentThread();
/// getMap 方法很简单:就是返回线程的threadLocals属性; 默认是null
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
然后我继续跟进 createMap方法,new 一个ThreadLocalMap对象
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
初始化一个Entry[] 数组,并new一个Entry对象,放入数组
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
我们来看看,Entry是个什么?
Entry继承了WeakReference,是一个弱指针引用对象,里面的弱引用对象就是for循环中new出来的ThreadLocal对象,这里可以理解为key,同时把set进来的 Byte[] 对象,设置给Entry的 value属性,这里是强引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
到这里,我们可以缕清楚set()方法的整体思路:
通过set方法,把Byte[]数组对象和ThreadLocal对象本身,包装成一个Entry对象,然后放到当前线程(这里是main线程)的threadLocals属性里。
所以,执行完set方法后,是当前线程对象(这里是main线程)持有了Byte[]的强引用和threadLocal对象的一个弱引用。
执行set方法后
第一个for循环结束后,栈中的强引用消失, threadLocal对象,只有entry的一个弱引用,随时可能被gc回收掉。而byte[]却一直被entry对象强引用,不会被gc回收。
两次for循环后,当前线程里面会有越来越多的entry,每个entry都会有一个Byte[1024*1024] ,还有可能有threadLocal对象(不发生gc这个对象就还在,会被回收)。所以随着for循环的不断执行,当前线程会持有越来越多的Entry对象,而这些空间不能被gc回收,最终造成OOM。
所以要避免这种情况发生应该怎么做?
那就是for循环结束后,调用一下threadLocal的remove方法,将entry清空,让gc回收掉。
/**
* @author KinYang.Lau
* @date 2021/3/30 8:17 下午
* -Xms20M -Xmx20M -Xmn10M
*/
public class ThreadLocalOOM {
public static void main(String[] args) {
System.out.println("main 线程开始...");
for (int i = 0; i < 100; i++) {
ThreadLocal<Byte[]> threadLocal = new ThreadLocal<>();
/// 在当前线程存入一个value
System.out.println("在线程:"+Thread.currentThread().getName()+
" 添加一个threadLocal");
threadLocal.set(new Byte[1024*1024]);
/// 方法解决,将threadLocal remove
threadLocal.remove();
}
System.out.println("main 线程完美运行结束");
}
}
总结
仔细看下ThreadLocal内存结构就会发现,Entry数组对象通过ThreadLocalMap最终被Thread持有,并且是强引用。也就是说Entry数组对象的生命周期和当前线程一样。即使ThreadLocal对象被回收了,Entry数组对象也不一定被回收,这样就有可能发生内存泄漏。ThreadLocal在设计的时候就提供了一些补救措施:
- Entry的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null(但是value不为null)。
所以在调用get()、set(T)、remove()等方法的时候,会自动清理key为null的Entity。 - remove()方法就是用来清理无用对象,防止内存泄漏的,所以每次用完ThreadLocal后需要手动remove()
有些文章认为是弱引用导致了内存泄漏,其实是不对的。
假设把弱引用变成强引用,这样无用的对象key和value都不为null,反而不利于GC,只能通过remove(方法手动清理,或者等待线程结束生命周期。
也就是说ThreadLocalMap的生命周期由持有它的线程来决定,线程如果不进入terminated状态,ThreadLocalMap就不会被GC回收,这才是ThreadLocal内存泄露的原因。