当前位置: 首页 > 科技观察

ThreadLocal的内存泄漏详解

时间:2023-03-15 13:32:37 科技观察

说到内存溢出,相信大家都知道是什么,但是说到内存泄漏,又是ThreadLocal,就不得不说到这里了,毕竟如果被问到面试的时候有没有可能是ThreadLocal的内存泄露问题没有很好理解?今天阿凡就来说说ThreadLocal内存泄露的原因,以及如何从开发上避免这个问题。什么是内存泄漏?说到内存泄漏,不得不说几句。这个对于初级和中级程序员来说可能比较陌生。为什么这么说是因为JVM有自己的内存回收机制,所以对于初中程序员和高级程序员来说,很少接触到这个,而内存泄漏的意思就是程序申请内存后,它不能释放已申请的内存空间。内存泄漏的危害可以忽略,但是内存泄漏累积的后果非常严重,不管再多的内存,迟早都会被用完。我们也都知道,有时候我们在定义变量的时候,应该明白他需要一块内存空间来存放这个数据信息,而如果这块内存不释放,就会导致内存被占用,而被占用的对象永远不能被回收,这就是内存泄漏。在说ThreadLocal的内存泄漏之前,我们先说说ThreadLocal的实现原理,然后我们分析一下,到底是什么原因导致泄漏的呢?privateThreadLocalMap(ThreadLocalMapparentMap){Entry[]parentTable=parentMap.table;intlen=parentTable.length;设置阈值(len);表=新条目[len];for(intj=0;jkey=(ThreadLocal)e.get();if(key!=null){对象值=key.childValue(e.value);条目c=新条目(键,值);inth=key.threadLocalHashCode&(len-1);while(table[h]!=null)h=nextIndex(h,len);表[h]=c;尺码++;}每个ThreadLocal维护一个ThreadLocalMap,key是使用弱引用的ThreadLocal实例,value是线程变量的副本。那么什么是弱引用呢?参考文献实际上分为两类。既然有弱引用,就必然会有强引用,所以我们要区分强引用和弱引用。强引用使用最普通的引用,一个对象具有强引用,不会被垃圾收集器回收。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误导致程序异常终止,也不愿回收这个对象。如果要取消强引用与对象的关联,可以显式地将引用赋值为null,这样JVM就会在合适的时候回收对象。当弱引用JVM进行垃圾回收时,无论内存是否充足,与弱引用关联的对象都会被回收。在java中,它由java.lang.ref.WeakReference类表示。缓存中可以使用弱引用。上面说过,每个ThreadLocal都维护一个ThreadLocalMap,key是使用弱引用的ThreadLocal实例,value是线程变量的副本。它们之间的引用关系如下:在这个图中,实线表示——强引用,虚线表示——弱引用。从图中我们可以分析出这个ThreadLocal是如何发生内存泄漏的。从图中我们可以知道,ThreadLocalMap使用的是ThreadLocal的弱引用作为key。如果一个ThreadLocal没有外部强引用,Key(ThreadLocal)会被GC回收,导致ThreadLocalMap中的key为null,而value仍然有强引用。只有Thead线程退出后,强引用价值链才会被打破。但是如果当前线程长时间没有结束,对于这些key为null的Entry的value总会有一个强引用链:ThreadRef->Thread->ThreaLocalMap->Entry->value这时候,它永远不会被回收,所以会导致ThreadLocal内存泄漏问题。这时候有读者会问为什么ThreaLocalMap使用ThreadLocal的弱引用而不是强引用。如果使用强引用,是不是就没有内存泄露的问题了?其实这根本不是一回事,因为如果此时ThreaLocalMap的key是强引用回收ThreadLocal,因为ThreadLocalMap还持有ThreadLocal的强引用,如果不手动删除,ThreadLocal就不会被回收回收,导致Entry内存泄漏。这是使用强引用的时候,但是使用弱引用呢?当ThreadLocalMap的key回收ThreadLocal进行弱引用时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使不手动删除,ThreadLocal也会被回收。当key为null时,下次ThreadLocalMap调用set()、get()、remove()方法时,value会被清除。所以这么看,不如改用弱引用。为什么?因为弱引用的使用可以多一层保护:对ThreadLocal的弱引用不会泄漏内存,下次ThreadLocalMap调用set()、get()、remove()时会清空对应的值。因此,ThreadLocal内存泄漏的根本原因是:由于ThreadLocalMap的生命周期与Thread一样长,如果不手动删除对应的key,就会导致内存泄漏,并不是因为弱引用。那我们继续看ThreadLocalMap中维护的ThreadLocalMap的源码分析staticclassThreadLocalMap{//hreadLocalMap中的数据存放在Entry类型数组的表中,Entry继承WeakReference(弱引用)staticclassEntryextendsWeakReference>{对象值;条目(ThreadLocal<?>k,对象v){超级(k);值=v;}}**成员变量**//初始容量privatestaticfinalintINITIAL_CAPACITY=16;//ThreadLocalMap数据实际存储在表中privateEntry[]table;//ThreadLocalMap表项个数privateintsize=0;//当达到这个大小时,扩大privateint阈值;ConstructorThreadLocalMap(ThreadLocalfirstKey,ObjectfirstValue){//初始化table数组,INITIAL_CAPACITY默认值为16table=newEntry[INITIAL_CAPACITY];//Key和16得到hash值inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);//创建节点,设置key-valuetable[i]=newEntry(firstKey,firstValue);大小=1;//设置扩展阈值setThreshold(INITIAL_CAPACITY);}我们再来看看他的remove方法;privatevoidremove(ThreadLocalkey){Entry[]tab=table;intlen=tab.length;inti=key.threadLocalHashCode&(len-1);//如果threadLocalHashCode计算出的下标找到的key和传入的key不一样,证明存在hash冲突,然后循环往下搜索(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){//如果key相同if(e.get()==key){//删除当前Entrye.clear();//清除expungeStaleEntry(i);返回;ThreadLocal中的ThreadLocalMap也有set和getEntry方法,还有很多,就不多介绍了。那么我们应该如何合理的使用ThreadLocal来保证内存不泄露呢?每次使用ThreadLocal时,都会调用它的remove()方法来清除数据,并且将ThreadLocal变量定义为privatestatic,这样ThreadLocal总是有一个强引用,保证了Entry的值可以随时通过ThreadLocal的弱引用访问value,然后清除。