理解ThreadLocal 可能产生的内存泄漏风险

标签: java  多线程并发编程

  内存泄漏(Memory Leak): 是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
  内存溢出(Out Of Memory): (简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件,而由系统配置、数据流、用户代码等原因而导致的内存溢出错误,即使用户重新执行任务依然无法避免。

一. ThreadLocal内存溢出的情况测试

1. 配置堆内存大小:

在这里插入图片描述

2. 程序:

/**
 * @author charles
 * @createTime 2020/6/7 1:01
 * @description 测试ThreadLocal 造成的内存泄漏
 */
public class ThreadLocalOOM {

    private static final int TASK_LOOP_SIZE = 500;
    /**
     * @author charles
     * @date 2020/6/7 1:02
     * @desc 初始化一个线程池
     */
    private static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,5,
            1, TimeUnit.MINUTES,new LinkedBlockingDeque<Runnable>());

    /**
     * @author charles
     * @date 2020/6/7 1:07
     * @desc 初始化一个大对象
     */
    static class LocalLargeVariable{
        //初始化一个5M的数组
        private byte[] bytes = new byte[1024*1024*5];
    }

    static final ThreadLocal<LocalLargeVariable> localVariable = new ThreadLocal<>();

    /**
     * @author charles
     * @date 2020/6/7 11:14
     * @desc 分三种情况进行测试
     */
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_LOOP_SIZE; i++) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println("use local variable");
                    // 场景一:
                    //new LocalLargeVariable();  //占用内存大概是25M
                    // 场景二:
                    //localVariable.set(new LocalLargeVariable());
                    // 场景三:
                    localVariable.set(new LocalLargeVariable());
                    // remove() 放在线程结束的时候使用,但并不是必须,可以加快内存回收
                    localVariable.remove();
                }
            });
            Thread.sleep(100);
        }
        System.out.println("thread execute over!");
    }
}

3. 内存图

   由情景一内存图:
在这里插入图片描述
   由情景二内存图:
在这里插入图片描述
   由情景三内存图:
在这里插入图片描述

4. 结果(使用JDK自带的内存工具Java VisualVM查看):

前提条件: TASK_LOOP_SIZE = 500
场景一: new LocalLargeVariable(); //占用内存大概是25M 线程池中有5个线程,
每个线程创建一个5M的对象,5*5=25M;线程执行结束,内存被回收
场景二: localVariable.set(new LocalLargeVariable()); //虽然set()方法也能置空Entry的引用,
等待GC回收,但并不那么及时 占用内存大概是110M
场景三: localVariable.set(new LocalLargeVariable()); localVariable.remove();
结尾使用remove() 方法//占用内存大概是20M

二. 原因分析

   情景一和情景三占用的内存差不多,情景二则占用了更多的堆内存,说明由使用了ThreadLocal发生了内存泄漏的情况。
   那么为什么会出现内存泄漏呢?
   根据分析我们知道每个Thread都维护一个ThreadLocalMap,而这个映射标的key就是ThreadLocal实例本身,value才是真正要存储的Object。
在这里插入图片描述
  也就是说 ThreadLocal本身是不存储数据,它只是作为一个key来让ThreadLocalMap获取值的。仔细观察ThreadLocalMap可以发现,这个map是持有WeakReference的ThreadLocal作为key的。也就是当GC发生时,key将会被回收。
   使用了 ThreadLocal 后,引用链如图所示:

在这里插入图片描述
   当threadLocal变量被置为null时,Heap中的threadLocal对象失去了引用,将被GC回收。同时Entry的key也将被回收。Entry中只剩下没有key的Value,此时存在强引用链CurrentThread Ref–>CurrentThread–>Map–>Value,若当前线程迟迟不能结束,则Heap中的Value始终不能被GC回收,造成内存泄漏。
   只有当CurrentThread 结束,出栈,强引用断开时,CurrentThread,Map,Value才会被GC回收。最好的办法在不使用ThreadLocal的时候,调用remove()方法清除数据。
   在调用get()方法和set()方法时,也会去调用expungeStaleEntry()方法来清除Entry中key为null的Value,但是这种清理是不及时,也不是每次执行都会调用的。因此也会引发内存泄漏的风险。只有remove()方法,显式调用了expungeStaleEntry()方法。
   从表面上引发ThreadLocal内存泄漏的原因是使用了弱引用,但是这里能够使用强引用吗?
   key使用强引用: 当引用了ThreadLocal的对象被回收的时候,ThreadLocalMap依然持有ThreadLocal的强引用。如果没有手动删除,ThreadLocal对象实例不会被回收,导致Entry内存泄漏。
   key使用弱引用: 当引用了ThreadLocal的对象被回收的时候,由于ThreadLocalMap 持有的是ThreadLocal的弱引用,即使没有手动删除,ThreadLocal对象实例也将被GC回收。在下一次ThreadLocalMap调用set,get,remove都有机会回收value。
   因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期与Thread的一样长,如果没有及时删除对应的key,就会导出内存泄漏,而不是因为使用了弱引用。
总结
   1. JVM利用设置ThreadLocalM的key为弱引用,来避免内存泄漏;
   2. JVM利用调用remove、get、set方法来回收弱引用;
   3. 当ThreadLocal存储很多的key为null的Entry的时候,再不去调用remove、get、set方法来回收弱引用,那么将导致内存泄漏;
   4. 使用 线程池 + ThreadLocal 时要小心,因为线程一直在不断地重复运行,从而积累更多的Value,从而导出内存泄漏。

版权声明:本文为qq_38675373原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_38675373/article/details/106599638