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

为什么每次用完ThreadLocal都要调用remove()

时间:2023-03-21 13:45:03 科技观察

请联系JerryCodes公众号转载本文。什么是内存泄漏KeyleakValueleak如何避免内存泄漏什么是内存泄漏?内存泄漏是指当一个对象不再有用时,占用的内存无法回收。因为通常情况下,如果一个对象不再有用,那么我们的垃圾收集器GC应该清理这部分内存。这样,这部分内存就可以重新分配到其他地方使用;否则,如果对象没用又不能被回收,如果这样的垃圾对象越积越多,就会导致我们。内存越来越少,最后出现内存不足的OOM错误。我们来分析下ThreadLocal中是如何发生这样的内存泄漏的。Key的泄露上一讲我们分析了ThreadLocal的内部结构,了解到每个Thread都有一个类似于ThreadLocal.ThreadLocalMap的类型变量,称为threadLocals。线程访问ThreadLocal后,会在其ThreadLocalMap中维护ThreadLocal变量与Entry中具体实例的映射关系。我们可能在业务代码中执行了ThreadLocalinstance=null操作,想要清理这个ThreadLocal实例,但是假设我们在ThreadLocalMap的Entry中强引用了ThreadLocal实例,那么,虽然在业务代码中ThreadLocal实例被设置为null,但是这个引用链在Thread类中还是存在的。GC在垃圾回收的时候会进行可达性分析,会发现ThreadLocal对象仍然是可达的,所以不会对这个ThreadLocal对象进行垃圾回收,从而造成内存泄漏。JDK开发者考虑到了这一点,所以ThreadLocalMap中的Entry继承了WeakReference弱引用,代码如下:staticclassEntryextendsWeakReference>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;Entry(ThreadLocalk,Objectv){super(k);value=v;}}可以看到这个Entry是extendsWeakReference。弱引用的特点是,如果对象只关联了弱引用,没有关联任何强引用,那么这个对象是可以被回收的,所以弱引用不会阻止GC。因此,这种弱引用机制避免了ThreadLocal的内存泄漏问题。这也是Entry的key使用弱引用的原因。Value的泄漏,但是,如果我们继续研究,我们会发现虽然ThreadLocalMap的每个Entry都是对key的弱引用,但是这个Entry中包含了对value的强引用,还是刚才的代码:staticclassEntryextendsWeakReference>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;Entry(ThreadLocalk,Objectv){super(k);value=v;}}可以看到value=v这行代码代表了一个强参考发生了。正常情况下,当线程终止时,key对应的value可以正常进行垃圾回收,因为不存在强引用。但是有时候一个线程的生命周期很长。如果线程不会在很长一段时间内终止,那么ThreadLocal及其对应的值可能就不再有用了。在这种情况下,我们应该确保它们可以正常回收。为了更好的分析这个问题,我们用下图来看具体的引用链接(实线代表强引用,虚线代表弱引用):可以看到,左边是引用栈,里面有栈中的一个ThreadLocal一个线程的引用和一个线程的引用,右边是我们的堆,堆内是对象的实例。让我们关注以下链接:ThreadRef→CurrentThread→ThreadLocalMap→Entry→Value→possibleleakedvalueinstances。这个链接随着线程的存在而存在。如果线程不停地执行耗时任务,那么垃圾回收进行可达性分析时,Value是可达的,所以不会被回收。但与此同时,我们可能已经完成了业务逻辑处理,不再需要这个Value了,这时候就会发生内存泄漏。JDK也考虑过这个问题。在执行ThreadLocal的set、remove、rehash等方法时,会扫描key为null的Entry。如果发现一个Entry的key为null,说明它对应的value没有作用,所以它会把对应的值设置为null,这样值对象就可以正常回收了。但是假设不再使用ThreadLocal,那么set、remove、rehash方法实际上是不会被调用的。这会导致值的内存泄漏。如何避免内存泄漏分析完这个问题,如何解决呢?解决方法就是我们课的题目:调用ThreadLocal的remove方法。调用该方法可以删除对应的值对象,可以避免内存泄漏。我们看一下remove方法的源码:publicvoidremove(){ThreadLocalMapm=getMap(Thread.currentThread());if(m!=null)m.remove(this);}可以看出它首先获取到ThreadLocalMapReferenced,调用它的remove方法。这里的remove方法可以把key对应的value清理掉,这样value就可以被GC回收了。因此,在使用ThreadLocal之后,我们应该手动调用它的remove方法,以防止内存泄漏。