ThreadLocal内存泄漏原理解析
1.什么是内存泄漏问题?
内存泄漏 表示就是我们申请了内存,但是该内存一直无法释放;
内存泄漏会导致内存溢出问题: 申请内存时,发现申请内存不足,就会报错 ;
2.在介绍ThreadLocal内存泄漏问题之前,我们先说一下Java中的四种引用类型:强引用,软引用,弱引用和虚引用。
强引用: 当内存不足时,JVM 开始进行 GC(垃圾回收),对于强引用对象,就算是出现了 OOM 也不会对该对象进行回收,死都不回收。
//定义一个强引用
Object o1 = new Object();
Object o2 = o1;
o1 = null;
System.gc();
System.out.println(o1);//null
System.out.println(o2);//不为空 java.lang.Object@1540e19d
o1=null;表示o1不再指向堆内存空间,但是o2还是指向堆内存空间的
软引用:当系统内存充足的时候,不会被回收;当系统内存不足时,它会被回收,软引用通 常用在对内存敏感的 程序中,比如高速缓存就用到软引用,内存够用时就保留,不够时就 回收。
Object object2 = new Object();
SoftReference<Object> objectSoftReference = new SoftReference<>(object2);
object2 = null;
try {
//申请30M的堆内存
byte[] bits = new byte[30 * 1024 * 1024];
}catch (Exception e){
}finally {
System.out.println(object2);//null
System.out.println(objectSoftReference.get());//java.lang.Object@1540e19d
}
当堆内存空间足够时,不会回收软引用对象objectSoftReference。
下面我们设置jvm运行参数,配置参数设置最大堆内存大小
-Xms5m -Xmx5m -XX:+PrintGCDetails
我们再运行发现objectSoftReference软引用对象被回收了
弱引用:弱引用需要用到 java.lang.ref.WeakReference 类来实现,它比软引用的生存周期更短。 对于只有弱引用的对象来说,只要有垃圾回收,不管 JVM 的内存空间够不够用,都会回收 该对象占用的内存空间。
Object object3 = new Object();
WeakReference<Object> objectWeakReference = new WeakReference<>(object3);
object3 = null;
System.gc();//执行垃圾回收 objectWeakReference对象会被回收掉
System.out.println(object3);//null
System.out.println(objectWeakReference.get());//null
虚:虚引用需要 java.lang.ref.Phantomreference 类来实现。顾名思义,虚引用就是形同虚设。 与其它几种引用不同,虚引用并不会决定对象的生命周期,我们不必深究。
3.ThreadLocal导致内存泄漏的原理
首页我们写一下ThreadLocal的用法
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.set("test");
System.out.println(stringThreadLocal.get());//test
ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。 Threadlocal 适用于在多线程的情况下,可以实现上下游的数据传递,实现线程隔离。
然后我们debug跟一下set方法的源码
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对象,和当前线程对象绑定,我们再断点跟一下map.set(this,value)
Entry对象存的是键和值映射关系,源码中定义了一个Entry数组,意思就是我们可以在一个线程中,定义很多个ThreadLocal对象
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.set("test");
System.out.println(stringThreadLocal.get());//test
ThreadLocal<String> stringThreadLocal2 = new ThreadLocal<>();
stringThreadLocal2.set("hello");
System.out.println(stringThreadLocal2.get());//hello
Entry数组中存的数据 用伪代码表示 即[<stringThreadLocal,"test">,<stringThreadLocal2,"hello">...]
这时我们执行stringThreadLocal = null,试想stringThreadLocal指向的堆内存空间,会被jvm垃圾回收掉吗?答案是:不会被回收
当我们执行stringThreadLocal = null时,变量stringThreadLocal不再指向堆内存,但Entry中的key是弱引用(见下方源码),所以如果当前线程一直存活,堆内存中的ThreadLocal就不会被清理,就会导致内存泄漏问题。即使线程执行结束,执行垃圾回收,Entry中key会为null,但value还是有值的
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
4.解决方案
方案a. 自己调用 remove 方法将不要的数据移除,避免内存泄漏的问题。原理:
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.set("test");
stringThreadLocal.remove();
stringThreadLocal=null;
先执行remove方法,则Entry中的K不再引用ThreadLocal对象,源码如下
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
然后再执行stringThreadLocal = null,stringThreadLocal也不再指向堆内存中的ThreadLocal对象,这样堆内存中的ThreadLocal对象就不被任何人引用了,JVM垃圾回收就会清理掉堆内存中的ThreadLocal对象
方案b.我们每次执行set方法时,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!
ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
stringThreadLocal.set("test");
stringThreadLocal = null;
ThreadLocal<String> stringThreadLocal2 = new ThreadLocal<>();
stringThreadLocal2.set("hello");
在执行stringThreadLocal2.set("hello");时,我们进入源码
发现如果某个Entry<K,V>的K不再指向堆内存中的ThreadLocal,会将该Entry移除掉
总结:存在内存泄露的有两个地方:ThreadLocal、Entry中Value;最保险还是要注意要自己及时调用remove方法。
更多推荐
所有评论(0)