前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解Threadlocal 关于内存泄漏的思考

深入理解Threadlocal 关于内存泄漏的思考

原创
作者头像
矿泉水
发布2018-05-11 12:01:52
1K1
发布2018-05-11 12:01:52
举报
文章被收录于专栏:风中追风

不知道经常使用  Threadlocal  的朋友有没有意识到内存泄漏这一点。

什么是内存泄漏呢?对象已经没有在其它地方被使用了,但是垃圾回收器没办法移除它们,因为还在被引用着。

我不用的对象,又不能被垃圾回收,就会造成内存泄漏。不了解垃圾回收的朋友看这篇文章:垃圾回收的细节

简单的拿个图表示下:

如果你了解垃圾回收机制,活着看过周志明老师的 深入理解java虚拟机 第二版, 你肯定 知道

强,软,弱,虚。四种引用关系。在进行GC时,只有强引用关系存在的对象才不会被垃圾回收。

而 ThreadLocalMapl里的 enter对象 继承了  jdk  WeakReference (弱引用API)提供的  的key ,也就是 ThreadLocal 取的是 WeakReference提供的弱引用对象,所以在GC时, ThreadLocal 会被垃圾回收期回收掉, Entry对象的key就为null了,然后 value 却是强引用 无法回收。

先上代码再上图~

[java] view plain copy

  1. static class ThreadLocalMap {  
  2. static class Entry extends WeakReference<ThreadLocal<?>> {  
  3. /** The value associated with this ThreadLocal. */
  4.            Object value;  
  5.            Entry(ThreadLocal<?> k, Object v) {  
  6. super(k);  
  7.                value = v;  
  8.            }  
  9.        }  

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 方法。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档