参考原文:

  1. https://cloud.tencent.com/developer/article/2355282
  2. https://www.zhihu.com/question/35250439
  3. https://blog.csdn.net/u013543848/article/details/102980066

内存泄露的场景

线程池由于线程复用导致的内存泄漏

线程池+static修饰ThreadLocal才会导致内存泄露

如果不使用static随时都可能被回收掉,这会导致不可预知的问题。

线程复用也就意味着线程中得ThreadLocalMap对象未被清理,这个对象引用的Entry也没被清理,Entry引用的value对象就不会被清理。这就导致了内存泄露。

此时,key无论是否存在强引用,都不影响value的内存泄露,key就是一个ThreadLocal对象,里面什么数据都没有,泄露一万个都不会产生多大的内存占用。

线程长时间执行:长时间不使用或忘记清除 ThreadLocal

只有在使用static修饰时才会导致内存泄露,如果不使用static修饰这个ThreadLocal,这个ThreadLocal作为的Key是个弱引用,随时会被清理掉,此时对应的值也随时会被ThreadLocal内部清理机制清理掉。

当前线程执行流程过长或者执行时间过长,此时如果ThreadLocal不使用了,还依旧放在那里,就会导致内存泄露。伴随着整个线程栈生命周期,线程一多,每个线程中都有这样一个对象,那占用的内存就会很多,也就是内存泄露了。

静态ThreadLocal变量

大众解读的原因:在使用完 ThreadLocal 后,如果没有显式调用 remove() 方法,那么即使当前线程结束,ThreadLocal 变量也不会被清理。ThreadLocal 是通过 ThreadLocalMap 来存储线程本地变量的,ThreadLocalMap 中的 Entry 使用了弱引用来存储 ThreadLocal 对象,但其值(即线程变量的副本)是强引用。如果 ThreadLocal 实例在代码中没有显式的强引用,并且没有手动调用 remove() 方法,那么 ThreadLocal 可能会被垃圾收集器回收,但 ThreadLocalMap 中的 Entry 会变成一个 keynull 的条目,导致 value(即线程变量的副本)不能被回收,从而引发内存泄露。

==不认同该观点。==

这个我不认可,这个ThreadLocal变量又不需要释放,每个线程中这个变量对应的值不一样,线程销毁变量就还在那里呗,又不影响什么,就那么一个固定的值,占用几字节空间啊。

ThreadLocalMap到底是ThreadLocal中的实例对象还是Thread中得实例对象?

ThreadLocal.ThreadLocalMap threadLocals = null;

从这行代码就可以看出来,ThreadLocalMap是Thread中得实例对象,Thread销毁了,threadLocals变量就不存在了,对 ThreadLocalMap 的引用就不存在了,ThreadLocalMap也不会存在了吧,那他引用的Entry还会存在吗?Entry中引用的Value也不存在了吧。

验证一下:ThreadLocal不调用remove,主线程继续执行,子线程执行完成已销毁,查看堆内存中的变量还有哪些,ThreadLocal引用的大对象是否还存在。

验证代码

public class ThreadLocalLeakExample {  
    public static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();  
  
    public static void main(String[] args) throws InterruptedException {  
        Thread thread = new Thread(() -> {  
            // 分配一个大的数组,模拟消耗内存的对象  
            byte[] bigData = new byte[1024 * 1024 * 100];  
            threadLocal.set(bigData); // 10MB  
            // 这里不调用 threadLocal.remove() 或者 threadLocal.set(null)        });  
        Thread.sleep(10*1000);  
        thread.start();  
        try {  
            thread.join();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
  
        // 线程结束,但如果不移除,10MB 的内存不会被回收  
  
        // 主线程不终止,我就想看看此时是否还占用着  
        while(true){}  
    }  
}

使用VisualVM观察堆区内存占用情况
image-20250107173518606

总结:线程结束,遇到GC,这个大对象直接就被清理掉了,这种情况并不会导致内存泄露。

有人说,Key可达导致Value可达,这里我有个很大的问题,Key可达等于Value可达吗?Value可达的原因在于可以通过Key找到Entry在ThreadLocalMap中的索引,通过这个索引能够找到Entry,通过Entry可以找到Key对应的Value,如果哈希冲突了,就得逐渐向后比较找到这个key对应的Entry,然后在找到Entry中的Value。Entry引用的Value,又不是Key引用的Value。线程消亡时ThreadLocalMap都会被回收了,Value根本就没法可达了,下一次GC时就被回收了。Key可不可达又有什么影响呢?

当一个对象弱引用和强引用同时存在,弱引用会随时断开吗?还是说弱引用并不会断开,只是这个对象随时可以被GC掉?强引用只是保证这个对象不会被GC掉,弱引用保证不了?

静态变量是阻碍了ThreadLocal内部的清理机制,也就是说,线程长时间运行时,弱引用的key可以随时被清理,然后value就可以随时被清理掉,避免了内存泄露。