ThreadLocal 到底会不会内存泄漏?实战直接告诉你答案!

相信很多人在使用ThreadLocal之前,看到过很多论坛中说ThreadLocal存在内存泄漏问题,也有些文章说ThreadLocal在最新版本中的set\get\rehash函数中加入了清除泄漏内存机制,只要后面get\set了,就不会存在内存泄漏的情况。大部分时间我们使用ThreadLocal并没有出现过内存泄漏问题,那ThreadLocal到底会不会发生内存泄漏呢?
答案是肯定的,ThreadLocal确实存在内存泄漏,只是内存泄漏是在某种使用不当的情况下才会发生,而这种使用不当的情况,只是我们很少遇到过而已,所以你会发现很多时候ThreadLocal并不会出现内存泄漏问题。
说了这么多,我们先来了解下,怎么使用不会出现内存泄漏,又怎么使用会出现内存泄漏。正确的使用方式有如下三种方式(在Springboot框架中使用Tomcat容器,所以请求到controller层是使用的worker线程池中的线程):
我们通过AB压测,可以看到老年代的内存使用率上升了,但通过GC之后,内存又降回去了,但依然存在内存占用问题。
1、AB压测:

2、压测之后:

3、垃圾回收之后:

这里还剩195MB的大小是因为我们在使用ThreadLocal设置value值后,虽然线程会在下次set时将原来的value覆盖掉,但是使用完之后并没有remove掉,所以在线程依然存活的情况下,这些value值都还在内存中,而且是强引用状态,无法回收。由于引用对象的线程依然存活,这种并不算内存泄漏。
我们通过AB压测,可以看到老年代的内存使用率上升了,但通过GC之后,内存降回压测之前的水平。
1、AB压测:

2、压测之后:

3、垃圾回收之后:

方式三中,我们通过AB压测,可以看到老年代的内存使用率上升了,但通过GC之后,内存降回压测之前的水平。
1、AB压测:

2、压测之后的堆内存:

3、GC之后的堆内存:

我们可以看到,正确使用ThreadLocal是不会有内存泄漏问题。那ThreadLocal会不会出现内存泄露呢?我们再来一起看看这样一种方式使用ThreadLocal的代码:
我们通过AB压测,可以看到老年代的内存使用率几乎100%了,而通过几次GC之后,已使用的内存无法回收掉。
1、AB压测:

2、压测后的堆内存使用情况:

3、垃圾回收之后,堆内存的仍然无法释放:

也就是说,ThreadLocal实例在方法中作为局部变量,没有使用remove方法清除之前设置的值,且线程依然存活时,就会出现内存泄漏问题。
这又是为什么呢?我们可以通过分析ThreadLocal源码发现,ThreadLocalMap使用ThreadLocal的弱引用作为key。
如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(在以上例子中,使用的是tomcat,所以工作线程依然存活),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
有同学说,线程池中的线程在下次使用ThreadLocal的set方法时,不就会将泄漏的value值清理掉吗?这里因为是局部变量,所以ThreadLocal的实例每次都是不一样的,哪怕是后面该线程再次set,由于创建的ThreadLocal实例不一样了, key值为null且value依然存在的Entry也无法清理掉的,所以内存泄漏了。
而作为全局变量时,外部会一直存在强引用,系统不会GC掉key值。当使用线程池的情况下,线程没有被销毁,所以value存在强引用,是一直不会被回收的,当在下一次set的时候,会将依然存在的Entry清理掉(包括key为null的Entry)。而在使用非线程池的情况下,每次都是创建一个新线程,当线程使用完被销毁时,此时当前线程的ThreadLocalMap也会被销毁,所以不会存在内存泄漏。

版权声明: 本文为 InfoQ 作者【刘超】的原创文章。
原文链接:【http://xie.infoq.cn/article/428bb2eafea3640ad6a97be5a】。文章转载请联系作者。
评论