1.什么是内存泄漏问题?

内存泄漏 表示就是我们申请了内存,但是该内存一直无法释放;

内存泄漏会导致内存溢出问题: 申请内存时,发现申请内存不足,就会报错 ;

2.在介绍ThreadLocal内存泄漏问题之前,我们先说一下Java中的四种引用类型:强引用,软引用,弱引用和虚引用

强引用: 当内存不足时,JVM 开始进行 GC(垃圾回收),对于强引用对象,就算是出现了 OOM 也不会对该对象进行回收,死都不回收。

        //定义一个强引用
        Object o1 = new Object();
        Object o2 = o1;
        o1 = null;
        System.gc();
        System.out.println(o1);//null
        System.out.println(o2);//不为空 java.lang.Object@1540e19d

 o1=null;表示o1不再指向堆内存空间,但是o2还是指向堆内存空间的

软引用:当系统内存充足的时候,不会被回收;当系统内存不足时,它会被回收,软引用通 常用在对内存敏感的 程序中,比如高速缓存就用到软引用,内存够用时就保留,不够时就 回收。

        Object object2 = new Object();
        SoftReference<Object> objectSoftReference = new SoftReference<>(object2);
        object2 = null;
        try {
            //申请30M的堆内存
            byte[] bits = new byte[30 * 1024 * 1024];
        }catch (Exception e){

        }finally {
            System.out.println(object2);//null
            System.out.println(objectSoftReference.get());//java.lang.Object@1540e19d
        }

当堆内存空间足够时,不会回收软引用对象objectSoftReference。

下面我们设置jvm运行参数,配置参数设置最大堆内存大小

-Xms5m -Xmx5m -XX:+PrintGCDetails

 我们再运行发现objectSoftReference软引用对象被回收了

 弱引用:弱引用需要用到 java.lang.ref.WeakReference 类来实现,它比软引用的生存周期更短。 对于只有弱引用的对象来说,只要有垃圾回收,不管 JVM 的内存空间够不够用,都会回收 该对象占用的内存空间。

        Object object3 = new Object();
        WeakReference<Object> objectWeakReference = new WeakReference<>(object3);
        object3 = null;
        System.gc();//执行垃圾回收 objectWeakReference对象会被回收掉
        System.out.println(object3);//null
        System.out.println(objectWeakReference.get());//null

虚:虚引用需要 java.lang.ref.Phantomreference 类来实现。顾名思义,虚引用就是形同虚设。 与其它几种引用不同,虚引用并不会决定对象的生命周期,我们不必深究。

3.ThreadLocal导致内存泄漏的原理

首页我们写一下ThreadLocal的用法

        ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
        stringThreadLocal.set("test");
        System.out.println(stringThreadLocal.get());//test

ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。 Threadlocal 适用于在多线程的情况下,可以实现上下游的数据传递,实现线程隔离。 

然后我们debug跟一下set方法的源码

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

底层其实就是一个ThreadLocalMap对象,和当前线程对象绑定,我们再断点跟一下map.set(this,value)

 Entry对象存的是键和值映射关系,源码中定义了一个Entry数组,意思就是我们可以在一个线程中,定义很多个ThreadLocal对象

        ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
        stringThreadLocal.set("test");
        System.out.println(stringThreadLocal.get());//test

        ThreadLocal<String> stringThreadLocal2 = new ThreadLocal<>();
        stringThreadLocal2.set("hello");
        System.out.println(stringThreadLocal2.get());//hello

Entry数组中存的数据 用伪代码表示 即[<stringThreadLocal,"test">,<stringThreadLocal2,"hello">...]

这时我们执行stringThreadLocal = null,试想stringThreadLocal指向的堆内存空间,会被jvm垃圾回收掉吗?答案是:不会被回收

 当我们执行stringThreadLocal = null时,变量stringThreadLocal不再指向堆内存,但Entry中的key是弱引用(见下方源码),所以如果当前线程一直存活,堆内存中的ThreadLocal就不会被清理,就会导致内存泄漏问题。即使线程执行结束,执行垃圾回收,Entry中key会为null,但value还是有值的

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

4.解决方案

方案a. 自己调用 remove 方法将不要的数据移除,避免内存泄漏的问题。原理:

        ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
        stringThreadLocal.set("test");
        
        stringThreadLocal.remove();
        stringThreadLocal=null;

先执行remove方法,则Entry中的K不再引用ThreadLocal对象,源码如下

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

然后再执行stringThreadLocal = null,stringThreadLocal也不再指向堆内存中的ThreadLocal对象,这样堆内存中的ThreadLocal对象就不被任何人引用了,JVM垃圾回收就会清理掉堆内存中的ThreadLocal对象

方案b.我们每次执行set方法时,会对key进行判断,如果key为null,那么value也会被设置为null,这样即使在忘记调用了remove方法,当ThreadLocal被销毁时,对应value的内容也会被清空。多一层保障!

        ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
        stringThreadLocal.set("test");
        stringThreadLocal = null;

        ThreadLocal<String> stringThreadLocal2 = new ThreadLocal<>();
        stringThreadLocal2.set("hello");

在执行stringThreadLocal2.set("hello");时,我们进入源码

 发现如果某个Entry<K,V>的K不再指向堆内存中的ThreadLocal,会将该Entry移除掉

总结:存在内存泄露的有两个地方:ThreadLocal、Entry中Value;最保险还是要注意要自己及时调用remove方法。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐