ThreadLocal内存泄露分析
感谢文章作者@Jd刘锟洋 的投稿。如果其他朋友也希望自己的 Java 和 Android 技术文章发表在 ImportNew,可以微博私信联系@ImportNew,或者邮箱联系 ImportNew.com@gmail.com。
看到有人在讨论ThreadLocal
,我产生了三个疑问:
ThreadLocal.ThreadLocalMap
中提到的弱引用,弱引用究竟会不会被回收?- 弱引用什么情况下回收?
- Java的
ThreadLocal
和在什么情况下会内存泄露?
带着这些疑问,自己模拟了一下ThreadLocal
中ThreadLocalMap
(ThreadLocalMap
本身并不是在Threadlocal
中,准确的说,是在Thread
类中,ThreadLocal
只不过是代理操作了当前Thread
中的ThreadLocalMap
)的结构,先展示下自己涉及的结构:

自己实现一个simple的ThreadLocalMap
,里面用一个entry
用来存放由自己模拟的ThreadLocal
调用set
方法set
进去的值。
并且和JDK的ThreadLocalMap
一样里面Entry
对象的key
用weakReference封装。
Main
方法如下:

设置运行参数:
-verbose:gc
看输出结果:

这里我已经模拟出了内存泄露的问题,可以看到FULL GC以后,内存还是被占用,且仔细观察可以看到,这个map
中的Key
已经有为null
了。换句话说你通过Key
已经不能获取到value
了,当然map.get(null)
也是可以的,不过Java里的ThreadLocal
不会这么去做,因为Map
中key==null
的元素可能不唯一。
从我的Main
方法中可以看到,我有th = null
的操作,但是还是有内存泄露,原因稍后分析。但有一点可以确定:th = null
在这里不能如我们想象的将ThreadLocal
th
的引用释放掉后,里面的key
,value
对象也释放。
可能会有疑问:
我这里持有了ThreadLocalMap
的引用tm所以不会回收,但实际上,手动设置Java的ThreadLocal
为null
时,当前线程任然持有ThreadLocalMap
的引用,所以不会回收我这里和Java是类似的。
回到刚开始提出的3个问题,一一解答:
1、ThreadLocal.ThreadLocalMap
中提到的弱引用,弱引用究竟会不会被回收?
会被回收,如上图所示。key
已经有null
的情况了。第一个Key
不为null
,原因在第二点。在经历过FULL GC后 所有的key
都被回收了。
2、弱引用什么情况下回收?
弱引用在GC(包括MinitorGC和Full GC)时,被扫描到就会被回收,但是有一个前提,该弱引用在外部没有被引用到(这个时候外部的引用等于强引用)。
换句话说,如果我Main
方法中持有一个key
的引用,哪怕他put
进Map
后被设置为弱引用的,也不会被回收。见下图

GC 日志:
3、Java的ThreadLocal
和在什么情况下会内存泄露?
答案是不会,原因如下图,在我们调用ThreadLocal.set()
的时候,会做一个将Key== null
的元素清理掉的工作,具体做法是:
第一步:ThreadLocalMap
拿threadLocalHashCode
与长度减一相与,求出哈希表的位置下图中的 i
。
第二步:编列Entry
,如果找到key
相等的,覆盖原值! 或者找到key==null
的,将值set
进去,并且将遍历时路过的key == null
的元素和他的value
都置为null
,,释放内存。
第三步:最后一个if
条件时,做rehash
的动作,即:将Entry
里的元素重新计算一下Hash
值,放到合适的位置去,猜想是为了加快下次访问的速度。
总结:
从这里看出,Java的ThreadLocal
对Key
使用到了弱引用,但是为了保证不再内存泄露,在每次set
、get
的时候主动对key == null
的entry
做遍历回收。
虽然不会造成内存泄露,但是因为只有在每次set
、get
的时候才会对entry
做key == null
的判断,从而释放内存,所以可能使大对象在内存中存活很长一段时间,从而占用内存。
所以,我们在使用完ThreadLocal
里的对象后最好能手动remove
一下,或者至少调用下ThreadLocal.set(null)
。
值得注意的是ThreadLocal
中的key
是当前当前ThreadLocal
自己,就像上面模拟的外部持有强引用的情况,ThreadLocal.ThreadLocalMap
中的key == null
情况很少出现,因为,大部分情况ThreadLocal
是以单例模式一直存在的。
附上 demo 源码:ThreadLocal_source