ThreadLocal内存泄露代码演示,和内存泄露原因分析

引发的内存泄漏分析
预备知识
引用

Object o = new Object();

这个o,我们可以称之为对象引用,而new Object()我们可以称之为在内存中产生了一个对象实例。

当写下 o=null时,只是表示o不再指向堆中object的对象实例,不代表这个对象实例不存在了。


强引用

一直活着:类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。


软引用

有一次活的机会:软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。


弱引用

回收就会死亡:被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。


虚引用

也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
内存泄漏的现象

执行下的ThreadLocalOOM,并将堆内存大小设置为-Xmx256m

public class ThreadLocalTest{
        public static final Integer SIZE = 500;
        static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1,
                TimeUnit.MINUTES, new LinkedBlockingDeque<>());
     
        static class LocalVariable {//总共有5M
            private byte[] locla = new byte[1024 * 1024 * 5];
        }
        public static void main(String[] args) {
            try {
                for (int i = 0; i < SIZE; i++) {
                    executor.execute(new Runnable() {
                        @Override
                        public void run() {
                            new LocalVariable();
                            System.out.println("开始执行");
                        }
                    });
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

我们启用一个线程池,大小固定为5个线程执行,在使用中的堆5*5M=25M

在jdk安装木的的/bin 下面,找到jvisualvm.exe,启动;选择正在运行的线程,选择监视,
情况1:使用普通的变量:可以看到使用的堆大小大概在25M左右浮动,堆的总大小是我们设置的256M

备注:在执行线程的时候,一定要加上Thread.sleep(100),如果不加上这个,看到的堆使用情况就是一条水平的直线

 

情况2:当我们启用了ThreadLocal变量以后:

 public class ThreadLocalTest1 {
        public static final Integer SIZE = 500;
        static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1,
                TimeUnit.MINUTES, new LinkedBlockingDeque<>());
     
        static class LocalVariable {//总共有5M
            private byte[] locla = new byte[1024 * 1024 * 5];
        }
     
        final static ThreadLocal<LocalVariable> local = new ThreadLocal<>();
        public static void main(String[] args) {
            try {
                for (int i = 0; i < SIZE; i++) {
                    executor.execute(new Runnable() {
                        @Override
                        public void run() {
                            local.set(new LocalVariable());
                            //new LocalVariable();
                            System.out.println("开始执行");
                        }
                    });
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

执行完成后我们可以看见,内存占用变为了100多M


情况3::加入一行代码ThreadLocal.remove(),再执行,看看内存情况:

public class MyThreadLocalOOM1 {
        public static final Integer SIZE = 500;
        static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 1,
                TimeUnit.MINUTES, new LinkedBlockingDeque<>());
     
        static class LocalVariable {//总共有5M
            private byte[] locla = new byte[1024 * 1024 * 5];
        }
     
        final static ThreadLocal<LocalVariable> local = new ThreadLocal<>();
        public static void main(String[] args) {
            try {
                for (int i = 0; i < SIZE; i++) {
                    executor.execute(new Runnable() {
                        @Override
                        public void run() {
                            local.set(new LocalVariable());
                            //new LocalVariable();
                            System.out.println("开始执行");
                            local.remove();
                        }
                    });
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

 

 

当添加上local.remove();,当我们启用了ThreadLocal以后确实发生了内存泄漏。
分析

        根据我们前面对ThreadLocal的分析,我们可以知道每个Thread 维护一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。仔细观察ThreadLocalMap,这个map是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

    static class Entry extends WeakReference<ThreadLocal<?>> {
                /** The value associated with this ThreadLocal. */
                Object value;
     
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }

因此使用了ThreadLocal后,引用链如图所示

 

图中的虚线表示弱引用。

        这样,当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,所以存在着内存泄露。

        只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。
解决内存泄露的方法:使用remove

        最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。
情况2 发生内存泄露的原因分析

        情况2中,由于Thread.sleep(100),可以清晰地看到,线程池中的任务执行完了,但是线程池中的五个线程依然会存在一段时间,因为我们没有显示调用remove()方法,所以导致五个线程中的new LocalVariable()没有被释放,发生了内存泄露。

                查看ThreadLocal可以知道,在set(),get()有些时候,会调用expungeStaleEntry()来清除Entry中key=null 的值,但是是不及时的,只有remove(),显示地调用了expungeStaleEntry()

既然使用弱引用导致了内存泄露,为什么还要使用弱引用而不是用强引用?
为什么使用弱引用而不是强引用?

下面我们分两种情况讨论:

key 使用强引用:

        对ThreadLocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的强引用(就是Entry中的key),如果没有手动删除,ThreadLocal的对象实例不会被回收,导致Entry内存泄漏。

key 使用弱引用:

        对ThreadLocal对象实例的引用被被置为null了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal的对象实例也会被回收。value在下一次ThreadLocalMap调用set,get,remove都有机会被回收。

        比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread(ThreadLocalMap是Thread的一个变量)一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。

        因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
总结

  •     JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
  •     JVM利用调用remove、get、set方法的时候,回收弱引用。
  •     当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
  •     使用线程池+ ThreadLocal时要小心,因为这种情况下,线程是一直在不断的重复运行的,从而也就造成了value可能造成累积的情况。
  • 一般使用线程池,要注意:共享的ThreadLocal只有一份,最好使用静态static和final修饰;在使用完毕后需要使用remove()方法释放弱引用(尤其是在多线程环境下)。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的ThreadLocal变量是一种线程本地变量,它提供了一种在多线程环境下保持变量的独立副本的机制。每个线程都可以独立地修改自己的副本,而不会影响其他线程的副本。这种机制可以避免线程安全问题,并且在某些情况下可以提高性能。 然而,如果在使用ThreadLocal变量时不小心处理,可能会导致内存泄漏内存泄漏是指在程序中不再使用的对象仍然占用内存空间,无法被垃圾回收器回收,从而导致内存的浪费。 为了避免ThreadLocal变量的内存泄漏,我们需要注意以下几点: 1. 及时清理:在使用完ThreadLocal变量后,应该及时调用remove()方法将其从当前线程中移除。这样可以避免变量的副本一直存在于线程中,占用内存。 2. 使用try-finally块:为了确保在任何情况下都能正确地清理ThreadLocal变量,可以使用try-finally块来确保在使用完后进行清理操作。 下面是一个示例代码演示了如何正确使用ThreadLocal变量并避免内存泄漏: ```java public class ThreadLocalExample { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { try { threadLocal.set("Hello, ThreadLocal!"); // 使用ThreadLocal变量 System.out.println(threadLocal.get()); } finally { // 清理ThreadLocal变量 threadLocal.remove(); } } } ``` 在上面的示例中,我们使用了try-finally块来确保在使用完ThreadLocal变量后进行清理操作。在finally块中调用remove()方法将变量从当前线程中移除,以避免内存泄漏。 总结一下,为了避免ThreadLocal变量的内存泄漏,我们需要在使用完后及时清理,并且可以使用try-finally块来确保清理操作的执行。这样可以保证ThreadLocal变量的副本不会一直存在于线程中,从而避免内存泄漏的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值