【Java面试】说说你对ThreadLocal内存泄漏问题的理解

2023-01-29 12:48:25

前置知识

讲解ThreadLocal的内存泄漏问题之前,首先得先知道什么是内存泄漏。
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

一文讲清强软弱虚四种引用类型

为什么会产生内存泄漏问题?

下面是大致的结构图,在栈中存放当前线程对象的引用以及ThreadLocal的引用。
他们指向了堆中的对象实例。
我们知道ThreadLocalMap(下文统称为Map)中的Entry的key使用的是ThreadLocal对象。
那么如果这里是一个强引用,那么如果我们设置ThreadLocal的引用为null,那么此时由于Map中的的Entry强引用了ThreadLocal作为键,因此此时会造成ThreadLocal无法被回收,在没有手动删除这个Entry或者当前线程仍然在运行的情况下,始终有强引用链 Thread的引用-》Thread对象-》Map-》Entry-》Key(ThreadLocal)和value-》内存。 此时就造成了内存泄漏。
因此如果ThreadLocalMap中的key使用了强引用,是完全无法避免内存泄漏的。
在这里插入图片描述
那么我们知道,ThreadLocalMap中的Entry继承了一个弱引用。也就是情况如下:
在这里插入图片描述
和上面不同的地方在于,在ThreadLocal的引用被回收之后,没有任何一个强引用指向ThreadLocal对象了,那么此时ThreadLocal就会被gc回收。因此此时Entry中的key为null。
但是,在没有手动删除这个Entry以及当前线程依旧运行的情况下,还是存在强引用链
Thread的引用-》Thread对象-》Map-》Entry-》Key(null)和value-》内存,value依旧不会被回收,而且这块value永远不会被访问到了,导致内存泄漏。
也就是说即使使用了弱引用,还是会导致内存泄漏。

如何解决内存泄露问题?

你可能很疑惑,都已经使用了弱引用了,为什么还是内存泄漏啊?
因为弱引用本身就不是为了解决这个问题而使用的。那么内存泄漏的真正原因是说明?
其实无非两个原因:

  • Entry没有被删除
  • 外部线程依旧在运行

第一点好理解,只要在使用完毕ThreadLocal之后调用remove方法删除Entry就可以避免内存泄漏。
第二点比较复杂,由于ThreadLocalMap是Thread的一个属性,并且被当前线程所引用,他的生命周期和Thread一样长,那么在使用完毕ThreadLocal的时候,Thread也随之结束,那么ThreadLocalMap自然也会被gc回收,也就从根源上避免了内存泄漏问题。
综上,内存泄露问题的根源是:由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应数据,而只是把ThreadLocal设定为空,就会导致内存泄漏。

为什么要使用弱引用?

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

  • 使用完ThreadLocal,调用其remove方法删除对应的Entry
  • 使用完ThreadLocal,当前Thread也随之运行结束

相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。
那么为什么key要用弱引用呢?
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null (也即是ThreadLocal为null )进行判断,如果为null的话,那么是会对value置为null的。
这就意味着使用完ThreadLocal , 当前线程依然运行的前提下,就算忘记调用remove方法,弱引用比强引可以多一层保障∶弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

ThreadLocal可能引起的OOM内存溢出问题简要分析

我们知道ThreadLocal变量是维护在Thread内部的,这样的话只要我们的线程不退出,对象的引用就会
一直存在。当线程退出时,Thread类会进行一些清理工作,其中就包含ThreadLocalMap,Thread调用
exit方法如下:

private void exit() {
        if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
            TerminatingThreadLocal.threadTerminated();
        }
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

但是,当我们使用线程池的时候,就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存
在的)。如果这样的话,将一些很大的对象设置到ThreadLocal中(这个很大的对象实际保存在Thread
的threadLocals属性中),这样的话就可能会出现内存溢出的情况。
一种场景就是说如果使用了线程池并且设置了固定的线程,处理一次业务的时候存放到
ThreadLocalMap中一个大对象,处理另一个业务的时候,又一个线程存放到ThreadLocalMap中一个大
对象,但是这个线程由于是线程池创建的他会一直存在,不会被销毁,这样的话,以前执行业务的时候
存放到ThreadLocalMap中的对象可能不会被再次使用,但是由于线程不会被关闭,因此无法释放Thread 中的ThreadLocalMap对象,造成内存溢出。也就是说,ThreadLocal在没有线程池使用的情况下,正常情况下不会存在内存泄露,但是如果使用了线程池的话,就依赖于线程池的实现,如果线程池不销毁线程的话,那么就会存在内存泄露。所以我们
在使用线程池的时候,使用ThreadLocal要格外小心!

  • 作者:ZuiaiLxh.
  • 原文链接:https://blog.csdn.net/Zhangsama1/article/details/128219635
    更新时间:2023-01-29 12:48:25