ThreadLocal内存泄露问题

468 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情

1.背景

ThreadLocal是线程内部的数据存储类,该存储类存储的对象只能在指定线程内部才能使用,其他线程无法获取

2.依赖的基础——常见的引用类型

JDK 1.2版本开始,对象的引用被划分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用软引用弱引用虚引用

Java中4种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用

16576be9ee015804tplv-t2oaga2asx-zoom-in-crop-mark1304000.webp

引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用当内存不足时对象缓存内存不足时终止
弱引用正常垃圾回收时对象缓存垃圾回收后终止
虚引用正常垃圾回收时跟踪对象的垃圾回收垃圾回收后终止

2.1 强引用(StrongReference)

强引用:某个对象没有指向它了,该对象就会被回收。否则,它永远不会被回收,直到OOM(OOM,全称“Out Of Memory”,翻译成中文就是“内存用完了”)

  • 测试代码
public class StrongReference {
​
    public static class Strong{
        @Override
        protected void finalize() throws Throwable {
            //Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
            System.out.println("finalize ....");
        }
    }
​
​
    public static void main(String[] args) throws IOException {
        Strong strong = new Strong();
        // 此时没有指向strong对象,则会被垃圾回收机制回收掉
//        strong = null;
        System.out.println("s=" + strong);
        System.gc();
        System.in.read();// 阻塞main线程,给垃圾回收线程时间执行
    }
}
  • 结果
s=null
finalize ....
s=com.ysl.domain.StrongReference$Strong@2e817b38

2.2 软引用(SoftReference)

软引用:空间够的时候任意分配,空间不够的时候软引用会被回收。

为了测试方便设置JVM启动参数-Xms20M -Xmx20M

img

  • 测试代码
public class SoftRef {
    public static void main(String[] args) throws InterruptedException {
        SoftReference<byte[]> softReference = new SoftReference<>(new byte[1024 * 1024 * 10]);
        System.out.println(softReference.get());
        Thread.sleep(500);
        System.out.println(softReference.get());
        // 若heap空间不够,会先引用软引用回收掉
        // 假如是强引用的话,会OOM
        byte[] b = new byte[1024 * 1024 * 10];
        System.out.println(softReference.get());
    }
}
  • 结果
[B@2e817b38
[B@2e817b38
null

2.3 弱引用(WeakReference)

弱引用:每次进行GC时会回收弱引用

弱引用软引用的区别在于:只具有弱引用的对象拥有更短暂生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现那些只具有弱引用的对象。

/**
 * @date: 2022/5/24
 * @FileName: WeakRef
 * @author: Yan
 * @Des:
 */
public class WeakRef {
    public static class MyObject{
        @Override
        protected void finalize() throws Throwable {
            System.out.println("finalizing myObject");
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        WeakReference<MyObject> wr = new WeakReference<>(new MyObject());
        System.out.println(wr.get());
        System.gc();
        Thread.sleep(1000);
        System.out.println(wr.get());
    }
}
com.ysl.threadlocal.entity.WeakRef$MyObject@8efb846
finalizing myObject
null

2.4 虚引用(PhantomReference)

  1. 虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期
  2. 如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
  3. 虚引用主要用来跟踪对象被垃圾回收器回收的活动。

为了测试方便设置JVM启动参数-Xms5M -Xmx5M

  • 测试代码

    /**
     * @date: 2022/5/24
     * @FileName: PhantomRef
     * @author: Yan
     * @Des:
     */
    public class PhantomRef {
        private static final List<byte[]> LIST = new ArrayList<>();
    ​
        private static final ReferenceQueue<Person> QUEUE = new ReferenceQueue<>();
    ​
        public static void main(String[] args) {
            PhantomReference<Person> personPhantomReference = new PhantomReference<>(new Person(), QUEUE);
            System.out.println(personPhantomReference.get());
    ​
            ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
            // 业务线程
            new Thread(() -> {
                while (true){
                    LIST.add(new byte[1024 * 1024]);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    System.out.println("业务对象的personPhantomReference"+ personPhantomReference.get());
                }
            }, "BussinessThread").start();
            
            // gc线程
            new Thread(() -> {
                while (true) {
                    Reference<? extends Person> poll = QUEUE.poll();
                    if (poll != null) {
                        System.out.println("--虚引用对象被jvm回收" + poll);
                    }
                }
            }, "GCThread").start();
        }
    }
    ​
    
  • 结果

    null
    --虚引用对象被jvm回收java.lang.ref.PhantomReference@2d46c162
    业务对象的personPhantomReferencenull
    业务对象的personPhantomReferencenull
    

3.ThreadLocal的应用场景

  1. 在同一个线程内参数的传递

  2. @Transactional

    在同一个数据库中要进行install和update操作,就要获取数据库连接的connection,而当前线程是在同一个事务中,要保证事务的一致性,则conncetion必须是同一个才能保证事务,这个时候就可以用ThreadLocal,把Connection放在ThreadLocal中来进行存储和传递

4.内存泄露

内存泄露分两种

4.1 ThreadLocal自身的内存泄露问题

解决方案:ThreadLocal定义成弱引用(在某种程度上解决key不为null导致的内存泄露问题),这样就可以成功被gc回收

image-20220524172657514

解释:

正常情况下我们的线程Thread里面会定义一个map,而这个map就是我们的ThreadLocalMap (通过阅读Thread的源码可以看到这一点)

image-20220524193927799

而这个ThreadLocalMap的key指向的是我们的ThreadLocal对象,而这个ThreadLocal中的set方法,在set的时候会指向一个this,this做为key,这个key指向的正是ThreadLocal

若把key的指向设置成强引用,然后把tl(ThreadLocal的一个对象实例)这个对象置为null,则此时,key是不会被gc回收的,这种情况下就会导致key的一个内存泄露问题,若设置成弱引用,每次gc的时候都可以被回收到;

而value值一般都是设置成强引用的,因为value存储的是我们的业务变量,一般情况下,业务变量是不允许其丢失的,所以是要设置为强引用,同样的这也会造成内存的泄露

image-20220524194833516

4.2线程池下的内存泄露

这种内存泄漏并不是ThreadLocal tl =null导致,而是因为线程没有被回收,但是我们又不想使用该变量了,进而造成了对象一直存在于ThreadLocalMap中没有被回收,从而导致了内存泄漏。

解决方案:ThreadLocal使用完成后,及时调用remove方法(主要是解决value强引用导致的内存泄漏问题),将其从ThreadLocaMap中移除,从而避免内存泄漏。

注意∶

使用线程池时,线程用完后一定要对ThreadLocalMap (每个线程都有一个ThreadLocalMap) 进行清除操作(ThreadLocalMap map= null,彻底清除,回收整个map),否则后面再拿到该线程的人都可以读到之前的数据,时间长了,ThreadLocalMap也会被填满。

资料来源

www.bilibili.com/video/BV1FU…

juejin.cn/post/684490…