ThreadLocal内存泄漏问题

184 阅读3分钟
1. Java的4大引用类型

ThreadLocal底层使用了弱引用设计,便于后续理解先对4大引用做下说明

  1. 强引用

    在jvm堆内存不够时,jvm宁愿抛出oom也不会回收强引用。

    public class ReferenceObject {
        //这个方法是垃圾回收之前会调用
        @Override
        protected void finalize() throws Throwable {
            System.out.println("对象被回收");
        }
    }
    // jvm 参数 -Xms5m -Xmx5m
    @Test
    public void strongReferenceTest(){
        //初始化字节数组,占用jvm内存空间
        byte[] bytes1 = new byte[1024 * 1024 * 2];
        //这是一个强引用
        ReferenceObject referenceObject = new ReferenceObject();
        //在new 这个字节数组时由于内存不够会发生gc,从打印结果看,抛出了异常
        byte[] bytes2 = new byte[1024 * 1024 * 3];
    }
    

    image.png

  2. 软引用

    jvm堆内存充足时软引用对象不会被回收可以正常使用,当jvm内存不充足时就会被回收。

    public class ReferenceObject {
        //这个方法是垃圾回收之前会调用
        @Override
        protected void finalize() throws Throwable {
            System.out.println("对象被回收");
        }
    }
    // jvm 参数 -Xms5m -Xmx5m
    @Test
    public void softReferenceTest(){
        //将ReferenceObject对象包装成软引用对象
        SoftReference<ReferenceObject> soft = new SoftReference(new ReferenceObject());
        //当在创建一个3m的字节数组时,内存不够会触发gc,finalize方法会被调用
        byte[] bytes = new byte[1024 * 1024 * 3];
    }
    

    image.png

  3. 弱引用

    只要gc发生弱引用就会被回收

    public class ReferenceObject {
        //这个方法是垃圾回收之前会调用
        @Override
        protected void finalize() throws Throwable {
            System.out.println("对象被回收");
        }
    }
    // jvm 参数 -Xms5m -Xmx5m
    @Test
    public void weakReferenceTest() throws Exception{
        WeakReference<ReferenceObject> soft = new WeakReference(new ReferenceObject());
        //调用gc,但是gc不会立即执行
        System.gc();
        //阻塞作用,无其他作用
        System.in.read();
    }
    

    image.png

  4. 虚引用

    虚引用必须与 ReferenceQueue 一起使用,当 GC 准备回收一个对象如果发现它是一个虚引用就会在回收前将它放到与之关联的ReferenceQueue中。利用这个原理,我们可以在检测到对象被回收时做些处理。

    public class ReferenceObject {
       //这个方法是垃圾回收之前会调用
       @Override
       protected void finalize() throws Throwable {
           System.out.println("对象被回收");
       }
    }
    // jvm 参数 -Xms5m -Xmx5m
    @Test
    public void phantomReferenceTest() throws Exception {
       List<byte[]> list = new ArrayList();
       ReferenceQueue queue = new ReferenceQueue();
       // 创建虚引用,要求必须与一个引用队列关联
       PhantomReference pr = new PhantomReference(new ReferenceObject(), queue);
       // 这个线程是不断填充堆内存,使其发生gc
       new Thread(() -> {
           while (true) {
               list.add(new byte[1024]);
           }
       }).start();
       // 判断虚引用即将要被回收,放到了队列中
       // 值得注意的是reference.get()方法只会会返回null,因为源码将此方法return null;
       new Thread(() -> {
           while (true) {
               PhantomReference reference = (PhantomReference) queue.poll();
               if (reference != null) {
                   System.out.println("虚引用被回收了:" + reference);
               }
           }
       }).start();
       System.in.read();
    }
    

    image.png

2. ThreadLocal
  1. ThreadLocal的结构

    ThreadLocal本身不存储数据,是使用ThreadLocalMap 存储数据。ThreadLocalMap内部维护一个Entry[]数组。在一个线程中每创建一个ThreadLocal,往ThreadLocal存放数据时就会new Entry对象,放到Entry数组中。每次set时使用int i = key.threadLocalHashCode & (len-1);来定位Entry数组的下标。如下图就是ThreadLocalMap中存储数据的table结构 ThreadLocalMap

  2. 分析如何造成内存泄漏的
    • 造成内存泄漏的前提是,这个线程运行时间比较长,迟迟不能结束,比如线程池内的work线程。
    • ThreadLocal采用弱引用引入,不管当前内存空间是否充足GC时将会回收掉该对象。假如使用强引 用,当ThreadLocal不再使用需要回收时,发现某个线程中ThreadLocalMap存在该ThreadLocal的 强引用,无法回收,造成内存泄漏。这种设计虽然能避免ThreadLocal在迟迟不结束的线程中造成内 存泄漏,但无法解决存放的对象造成内存泄漏。
    • 由于ThreadLocalMap和线程的生命周期是一致的,当线程迟迟不结束,由于value是强引用, 会驻留在线程的ThreadLocalMap的Entry中。虽然key(ThreadLocal)被回收,但value却存在 Entry中,形成key=null,value!= null的无效Entey存在ThreadLocalMap的table中。随着时 间的推移这样的Entry越来越多而导致内存泄漏。
    • 这种情况下,一般在使用完ThreadLocal后显示的调用remove();方法