ThreadLocal --弱引用和内存泄漏

回顾概念​

我们先来回顾这个问题中涉及的几个名词概念,再来分析问题。

 内存泄漏相关概念

  内存泄露 (memory leak),是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。就相当于你租了个带钥匙的柜子,你存完东西之后把柜子锁上之后,把钥匙丢了或者没有将钥匙还回去,那么结果就是这个柜子将无法供给任何人使用,即无法被垃圾回收器回收。

  内存溢出 (out of memory),是指程序在申请内存时,没有足够的内存空间供其使用(杯子就只能装500ml的水,都满了,你还想加水),出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。你申请了一个盘子用尽各种方法只能装4个果子,你却装了5个,结果多的那个掉到地上不能吃了。这就是溢出!比方说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也产生空间溢出,称为下溢。就是分配的内存不足以放下数据项序列,称为内存溢出.

  内存泄露 (memory leak)会最终会导致内存溢出 (out of memory)

 弱引用相关概念

  在传统的引用定义下,一个对象只有“被引用”或者“未被引用”两种状态。我们希望能描述一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象。于是将引用分为强引用、软引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。

强引用:是指引用赋值,好比Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

弱引用:描述那些非必须的对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止,无论当前内存是否足够,都会被回收。

《深入理解Java虚拟机》中介绍

 

弱引用和内存泄漏关系

在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,接下来我们分析内存泄漏跟Entry中使用了弱引用的key是否有关系。

如果key使用强引用

​ 假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?

​ 此时ThreadLocal的内存图(实线表示强引用)如下:

 

假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。

在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

 如果key使用弱引用

那么ThreadLocalMap中的key使用了弱引用,会出现内存泄漏吗?

​ 此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:

 

同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

​由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。

​ 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

​ 也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

 出现内存泄漏的真实原因?

比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?

在以上两种内存泄漏的情况中,都有两个前提:

  1. 没有手动删除这个Entry。

  2. CurrentThread依然运行。

​ 第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。

​ 第二点稍微复杂一点, 由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏

 为什么使用弱引用?

根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。

​ 要避免内存泄漏有两种方式:

  1. 使用完ThreadLocal,调用其remove方法删除对应的Entry。

  2. 使用完ThreadLocal,当前Thread也随之运行结束。

 相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。

  也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?

​   事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。

​   这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏

 

posted @ 2022-06-18 16:07  JustJavaIt  阅读(765)  评论(0编辑  收藏  举报