ThreadLocal内存泄漏案例分析实战

用代码实战,彻底搞清楚ThreadLocal发生内存泄漏的情况。很多文章讲的模棱两可,在和群友的沟通中,基本弄清楚了ThreadLocal到底是什么回事,解决大多数文章都无法把知识点和实际使用结合起来讲。

先写个小例子

/**
 * 测试threadLocal内存泄漏
 * 01:固定6个线程,每个线程持有一个变量
 * 按理来说会有 6 * 5 = 30M内存无法回收,其余的在set方法中覆盖了。
 */
public class ThreadLocalOutOfMemoryTest {
    static class LocalVariable {
        //总共有5M
        private byte[] locla = new byte[1024 * 1024 * 5];
    }

    // (1)创建了一个核心线程数和最大线程数为 6 的线程池,这个保证了线程池里面随时都有 6 个线程在运行
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(6, 6, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)创建了一个 ThreadLocal 的变量,泛型参数为 LocalVariable,LocalVariable 内部是一个 Long 数组
    static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        // (3)向线程池里面放入 50 个任务
        for (int i = 0; i < 50; ++i) {
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    // (4) 往threadLocal变量设置值
                    LocalVariable localVariable = new LocalVariable();
                    // 会覆盖
                    ThreadLocalOutOfMemoryTest.localVariable.set(localVariable);
                    // (5) 手动清理ThreadLocal
                    System.out.println("thread name end:" + Thread.currentThread().getName() + ", value:"+ ThreadLocalOutOfMemoryTest.localVariable.get());
//                    ThreadLocalOutOfMemoryTest.localVariable.remove();

                }
            });

            Thread.sleep(1000);
        }

        // (6)是否让key失效,都不影响。只要持有的线程存在,都无法回收。
        //ThreadLocalOutOfMemoryTest.localVariable = null;
        System.out.println("pool execute over");
    }
}
复制代码
  1. 先打开jdk工具包下面的jvisualvm.exe,在运行这个小例子。等待程序结束,主动触发一次垃圾回收。

ThreadLocal内存泄漏案例分析实战

我们可以看到,大概还有30M的内存没有回收。和我们预期的结论一致。

a.为什么不是50*5=250M 呢?

因为java.lang.ThreadLocal#set()是会覆盖的,每个线程持有一份,同一个线程执行多次,由于key是同一个ThreadLocal变量,所以会路由到数组的同一个位置上,直接覆盖上次的value。

b.为什么无法被回收掉30M的内存呢?

因为线程池中的6个线程存活,对ThreadLocalMap持有强引用

  1. 打开第5处的代码,再次运行观察堆内存:

ThreadLocal内存泄漏案例分析实战

这个很好理解,因为我们手动调用了remove方法,清理了ThreadLocalMap中的对象(把Entry对象指向了null),没有了强引用,当然直接被回收。

  1. 关闭第5处代码,再打开第6处代码:

ThreadLocal内存泄漏案例分析实战

和第一种情况一致,任然还有30M的内存无法回收。所以ThreadLocalMap的回收其实和Entry对象的Key是弱引用没有太大关系

只要持有ThreadLocalMap的线程存活,不管Key失效或者未失效,value都不会被回收。所以才要求我们使用的过程中要即时清理。

ThreadLocalMap的整体结构

ThreadLocal内存泄漏案例分析实战

  1. ThreadLocalMap是ThreadLocal的内部类,内部维护了一个Entry类型的table数组。
  2. Entry对象由Key和Value两个成员变量,key是对ThreadLocal对象的弱引用③,Value是针对这个threadLocal存入的Object类型对象。
  3. key弱引用threadLocal,每次GC,如果threadLocal对象只剩下被这个ThreadLocalMap的key弱引用, 那么对象将被回收掉,key为null值。
  4. 程序对ThreadLocal变量的强引用,当这个强引用消失的时候,threadLocal将会被回收。类似与我们代码:
ThreadLocal local = new ThreadLocal();
local = null;
复制代码
  1. 线程Thread对象对整个ThreadLocalMap持有的强引用。

问题:③这里为什么要使用弱引用呢?

假设一种在Tomcat线程池使用ThradLocal的场景,线程持有对ThreadLocalMap的强引用,导致ThreadLocalMap的生命周期很长。假设为每一个请求创建一个ThreadLocal对象用于存储session,如果不及时销毁,整个ThreadLocalMap占用的内存会越来越大。 提前把Key设置为thradLocal对象的弱应用,当程序不在引用threadLocal对象的时候,gc就可以快速回收掉thradLocal对象,Entry的key为null。在ThreadLocalMap#set方法中会清理key=null的Entry对象,以达到回收内存的目的。

小结

  • Thread -> ThreadLocalMap是一对一,一个线程维护一个ThreadLocalMap。
  • Thread -> ThreadLocal是一对多,一个线程可以拥有多个ThreadLocal对象,分别hash映射到数组不用的索引。
  • Entry中key的弱引用可以看成是协助清理ThreadLocalMap中的Entry键值对的一种方法。但是需要手动把threadLocal变量设置为null:threadLoca=null
  • ThreadLocal内存泄漏和key采用弱引用的关系不大,因为实际编码中没人这样编码threadLoca=null,所以是否回收ThreadLocalMap主要取决于线程的生命周期。
  • 在线程长生命周期的场景中,用完变量后,要调用ThreadLocalMap#remove()主动清理。


作者:城南码农
链接:https://juejin.cn/post/6982121384533032991
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上一篇:ThreadLocal


下一篇:ThreadLocal的介绍+经典应用场景