不知道经常使用 Threadlocal 的朋友有没有意识到内存泄漏这一点。
什么是内存泄漏呢?对象已经没有在其它地方被使用了,但是垃圾回收器没办法移除它们,因为还在被引用着。
我不用的对象,又不能被垃圾回收,就会造成内存泄漏。不了解垃圾回收的朋友看这篇文章:垃圾回收的细节
简单的拿个图表示下:
如果你了解垃圾回收机制,活着看过周志明老师的 深入理解java虚拟机 第二版, 你肯定 知道
强,软,弱,虚。四种引用关系。在进行GC时,只有强引用关系存在的对象才不会被垃圾回收。
而 ThreadLocalMapl里的 enter对象 继承了 jdk WeakReference (弱引用API)提供的 的key ,也就是 ThreadLocal 取的是 WeakReference提供的弱引用对象,所以在GC时, ThreadLocal 会被垃圾回收期回收掉, Entry对象的key就为null了,然后 value 却是强引用 无法回收。
先上代码再上图~
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
super ( k ); k 是从 weakReference里得到的,弱引用无疑,value ,自己的Object ,
一般都是强引用。
把它们的 堆栈图 画出来,让大家更好的理解:
这个图应该阐述得很清楚了~
每个Thread都有自己的 一个 ThreadLocalMap。 key 是 TreadLocal 实例对象, value 就是 你要保存的那个 变量。
图中红色部分描述的了 ThreadLocalMap 是通过WeakReference 包装了 TreadLocal ,取的是 TreadLocal的弱引用 对象。
那么在GC 的时候就会造成 TreadLocal 肯定就会被回收掉。 Entry对象的key就为null了,然后 value 却是强引用 无法回收。
如果这个方法又长时间不结束的话,就有会这么一条 强引用的 GCROOT 引用链 的存在:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value ; value 就一直不会被回收, 因为它的 另一半 key 已经不存在了,所以它也不会被调用。 这就造成了内存泄漏。
但是!这可是大名鼎鼎的JDK诶,1.9都出来了,肯定考虑到这个点了,于是在1.5的时候加入了remove 方法 解决这个问题 ;
后来又针对:怕部分程序员还是忘记调用remove 方法,又在get方法 中做了优化, 我们看看源码,是哪里优化了:
这里就不贴源码了, 认真的朋友可以进入自己的 开发工具 跟着下面 的 注释,一个一个的点进去看,一目了然。
源码的进入姿势是 ThreadLocal.class 的 get( ) -> ThreadLocalMap 的 getEntry ( this ) 方法,
当key 不满足 判断条件时, 进入 getEntryAfterMiss(key, i, e) 方法 ;
当key == null -> expungeStaleEntry(i); 如果 k == null 时, e.value = null;
看到这里,意思就是在Threadlocal 在调用get 方法的时候,如果key 是 null 就会把 value 的强引用关系清除掉。
这样 Threadlocal 被垃圾回收掉的时候 保存的 副本变量 也会被 垃圾回收 从而避免了 部分次数的 内存泄漏。
但这并不能,完全的避免内存泄漏, 仍然需要我们在 调用set 方法后 显示的 调用remove()方法。
remove 方法 的源码 , 其实就是清除了 entry 对象的引用 关系,
然后又调用了 expungeStaleEntry(i) 方法,key ==null时 , e.value =null ;
所以我们应该有意识的形成良好的编程习惯/规范,在使用完ThreadLocal之后,记得调用一下remove方法。从而避免内存泄漏。
到这里,ThreadLocal 造成内存泄漏的原因以及解决办法以及分析完了。
上一篇中 <一>深入理解面试常问的Threadlocal的实现原理 提到了 主题内容的第三部分也分析完了。
我们再来进行主题四:思考 和 总结 学习的 这个 ThreadLocal;
先来思考一个问题: 我们知道了内存泄漏是因为 ThreadLocalMap 中 entry 对象的 key 去的是 ThreadLocal的弱引用对象。
那我是不是将 ThreadLocal 的弱引用 换成 强引用 就不会引起内存泄漏了呢?
于是我们拿 key 取弱引用对象 跟 强引用对象 做个对比,再分析分析优缺点~
key 是 强引用:
如果我们从 ThreadLocal 里面 已经取到了我们想要的 线程副本 value ,我们是不是就希望 ThreadLocal能够被垃圾回收掉呢? 但是因为 ThreadLocalMap 中的 entry 还持有对 ThreadLocal 的强引用。 所以导致 ThreadLocal 迟迟都不能被垃圾回收。所以value 也不能被垃圾回收,从而造成了 entry 对象 发生内存泄漏。
key 是 弱引用:
首先我们看key~ ThreadLocal 被垃圾回收时,就算 ThreadLocalMap 持有 ThreadLocal 的引用也没有关系,这是一种弱引用关系, 即使我们没有手动的将 ThreadLocal 设置为 null ,垃圾回收器还是会将 ThreadLocal 回收掉。
再看value~ value 在调用get remove 方法的时候也会被垃圾回收。
对比分析后,我们可以发现,如果key 是弱引用,我调用 remove 方法 就能避免value 对象的内存泄漏。
如果key 是强引用,我用完了 ThreadLocal 我还得将 ThreadLocal 设置为null,value也设置为null
最后发现:哦~造成内存泄漏的根本原因并不是弱引用关系所导致的,真正的原因是:(这里我们提到一个生命周期的概念)
ThreadLocalMap和Thread的生命周期一样长,而 ThreadLocal 实际上较短(因为我用完就不需要它了)。
在没有手动的删除key 的情况下,就会造成泄漏, JDK 现在用的弱引用 优化了 在程序员失误的情况下,我只内存泄漏value,
并且提供了不泄漏value 的 API 方法 :显示调用 remove方法。而用强引用, 那我key 和 value 全部都可能内存泄漏。
那么不知道大家是否想起了其它情况下的内存泄漏,比如集合类,数据库资源那些的。
其根本问题全在这儿:引用方和被引用方的生命周期长短不一致导致的。 这算是对多种情况的一个上层抽象吧~
这么分析了一波 ThreadLocal 能给我们带来什么。
1、了解了 ThreadLocal 的实现原理,从而能更好的使用 ThreadLocal ,能避免内存泄漏的情况。
2、能规范我们的编码习惯,并抽象出了内存泄漏的原因,以后编码时有意识考虑这些问题。
3、ThreadLocal 能实现每个线程都有一份变量副本,其实就是空间换时间的设计思路,因为每个线程都有个ThreadLocalMap
从而实现了 另一种意义上的 “无锁编程”。
4、 你懂的~
最后 ThreadLocal 就跟加锁后要释放锁一样的, 用完记得调用 remove 方法。
希望大家看完有所收获,同时 资历尚浅,还请多多指正。