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

用户头像
刘超
关注
发布于: 2020 年 05 月 14 日
ThreadLocal到底会不会内存泄漏?实战直接告诉你答案!

​相信很多人在使用ThreadLocal之前,看到过很多论坛中说ThreadLocal存在内存泄漏问题,也有些文章说ThreadLocal在最新版本中的set\get\rehash函数中加入了清除泄漏内存机制,只要后面get\set了,就不会存在内存泄漏的情况。大部分时间我们使用ThreadLocal并没有出现过内存泄漏问题,那ThreadLocal到底会不会发生内存泄漏呢?

 

答案是肯定的,ThreadLocal确实存在内存泄漏,只是内存泄漏是在某种使用不当的情况下才会发生,而这种使用不当的情况,只是我们很少遇到过而已,所以你会发现很多时候ThreadLocal并不会出现内存泄漏问题。

 

说了这么多,我们先来了解下,怎么使用不会出现内存泄漏,又怎么使用会出现内存泄漏。正确的使用方式有如下三种方式(在Springboot框架中使用Tomcat容器,所以请求到controller层是使用的worker线程池中的线程):

//方式一:设置为全局变量
private static ThreadLocal<Byte[]> threadSession = new ThreadLocal<Byte[]>();
@RequestMapping("/test")
public ResponseMessage test() {
ResponseMessage response = new ResponseMessage();
try {
threadSession.set(new Byte[4096*1024]);
response.setMessageEnum(ReturnEnumCode.ACTION_SUCCESS);
} catch (Exception e) {
response.setMessageEnum(ReturnEnumCode.E_SYSTEM_ERROR);
}
return response;
}

 

我们通过AB压测,可以看到老年代的内存使用率上升了,但通过GC之后,内存又降回去了,但依然存在内存占用问题。

1、AB压测:

       

       

 

2、压测之后:

       

       

 

3、垃圾回收之后:

       

       

 

这里还剩195MB的大小是因为我们在使用ThreadLocal设置value值后,虽然线程会在下次set时将原来的value覆盖掉,但是使用完之后并没有remove掉,所以在线程依然存活的情况下,这些value值都还在内存中,而且是强引用状态,无法回收。由于引用对象的线程依然存活,这种并不算内存泄漏。



//方式二:局部变量时,需要使用完之后remove掉
@RequestMapping("/test")
public ResponseMessage test() {
ResponseMessage response = new ResponseMessage();
ThreadLocal<Byte[]> threadSession = new ThreadLocal<Byte[]>();
try {
threadSession.set(new Byte[4096*1024]);
response.setMessageEnum(ReturnEnumCode.ACTION_SUCCESS);
} catch (Exception e) {
response.setMessageEnum(ReturnEnumCode.E_SYSTEM_ERROR);
}finally {
threadSession.remove();
}
return response;
}

 

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

1、AB压测:

       

       

 

2、压测之后:

       

       

 

3、垃圾回收之后:

       

  

//方式三:在新创建的线程中使用局部变量时,使用完之后没有remove掉
@RequestMapping("/test")
public ResponseMessage test() {
ResponseMessage response = new ResponseMessage();
try {
new Thread() {
ThreadLocal<Byte[]> threadSession = new ThreadLocal<Byte[]>();
public void run() {
threadSession.set(new Byte[4096*1024]);// 为线程添加变量
}
}.start();
response.setMessageEnum(ReturnEnumCode.ACTION_SUCCESS);
} catch (Exception e) {
logger.error("checkIsExistOrder",e);
response.setMessageEnum(ReturnEnumCode.E_SYSTEM_ERROR);
}finally {
}
return response;
}



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

1、AB压测:

       

       

 

2、压测之后的堆内存:

       

       

 

3、GC之后的堆内存:

       

       

 

我们可以看到,正确使用ThreadLocal是不会有内存泄漏问题。那ThreadLocal会不会出现内存泄露呢?我们再来一起看看这样一种方式使用ThreadLocal的代码:

//方式一:局部变量时,使用完之后没有remove掉
@RequestMapping("/test")
public ResponseMessage test() {
ResponseMessage response = new ResponseMessage();
ThreadLocal<Byte[]> threadSession = new ThreadLocal<Byte[]>();
try {
threadSession.set(new Byte[4096*1024]);
response.setMessageEnum(ReturnEnumCode.ACTION_SUCCESS);
} catch (Exception e) {
response.setMessageEnum(ReturnEnumCode.E_SYSTEM_ERROR);
}
return response;
}

我们通过AB压测,可以看到老年代的内存使用率几乎100%了,而通过几次GC之后,已使用的内存无法回收掉。

 

1、AB压测:

       

       

 

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

       

       

 

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

       

       

 

也就是说,ThreadLocal实例在方法中作为局部变量,没有使用remove方法清除之前设置的值,且线程依然存活时就会出现内存泄漏问题。

 

这又是为什么呢?我们可以通过分析ThreadLocal源码发现,ThreadLocalMap使用ThreadLocal的弱引用作为key。

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

如果一个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也会被销毁,所以不会存在内存泄漏。





发布于: 2020 年 05 月 14 日 阅读数: 49
用户头像

刘超

关注

还未添加个人签名 2018.09.08 加入

一线互联网码农,极客时间《Java性能优化实战》作者

评论

发布
暂无评论
ThreadLocal到底会不会内存泄漏?实战直接告诉你答案!