【内存泄漏】测试ThreadLocal--gc后引发的threadLocalMap的key为null,但value不为null的情况

探讨了ThreadLocal在Java中如何工作,特别是在GC后的表现。解释了为何在GC后,ThreadLocalMap中的key可能变为null,而value仍然存在,并提供了代码示例来演示这一现象。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

效果

发生gc后,key为null,value不为null。
注意:这里立即释放了对threadLocal实例的强引用,帮助gc回收查看弱引用的使用方法

在这里插入图片描述

原因

ThreadLocal#set后会将threadLocal实例本身作为key 放入 Thread.currentThread().threadLocalMap中,与threadlocal#set的value构成一对Entry。而Entry使用了threadLocal的实例作为 弱引用。因此当发生gc的时候,弱引用的key会被回收掉,而作为强引用的value还存在。

作为key的弱引用的ThreadLocal
在这里插入图片描述
这里借用网图帮助理解
在这里插入图片描述

题外话

如果没有失去对ThreadLocal本身的强引用,那么不会回收threadLocal。(注释中有标注helpGC的地方)
而我们平时代码中,常常使用static final修饰threadLocal保留一个全局的threadLocal方便传递其他value(threadLocal一直被强引用),作为key的threadLocal就不会被回收,更不会导致key为null。

public static final ThreadLocal<Object> THREAD_LOCAL =  new ThreadLocal<>();

使用ThreadLocal关键之处还是在于:使用完毕要记得remove。特别是在线程池中使用的时候。(否则会等到下一次set的时候才替换掉value–>set的key为同一个threadLocal对象,所以是替换)

代码

threadLocal被强引用 引用,无法被回收
在这里插入图片描述

/**
 * 测试ThreadLocal 在gc后引发的threadLocalMap的key为null,但value不为null的情况
 * @Author thewindkee
 * @Date 2019/12/27 9:28
 */
public class Test {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        Thread t = new Thread(()->test("abc",false));
        t.start();
        t.join();
        System.out.println("--gc后--");
        Thread t2 = new Thread(() -> test("def", true));
        t2.start();
        t2.join();
    }

    private static void test(String s,boolean isGC)  {
        try {
            ThreadLocal<Object> threadLocal = new ThreadLocal<>();
            threadLocal.set(s);
            threadLocal = null;//失去对threadLocal的强引用 ,help gc
            if (isGC) {
                System.gc();
            }
            Thread t = Thread.currentThread();
            Class<? extends Thread> clz = t.getClass();
            Field field = clz.getDeclaredField("threadLocals");
            field.setAccessible(true);
            Object threadLocalMap = field.get(t);
            Class<?> tlmClass = threadLocalMap.getClass();
            Field tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object[] arr = (Object[]) tableField.get(threadLocalMap);
            for (Object o : arr) {
                if (o != null) {
                    Class<?> entryClass = o.getClass();
                    Field valueField = entryClass.getDeclaredField("value");
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    valueField.setAccessible(true);
                    referenceField.setAccessible(true);
                    System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

本文首次发布于2019年12月27日

强引用、软引用、弱引用、虚引用测试
ThreadLocal为什么会内存泄漏

<think>好的,我现在要帮用户解答关于ThreadLocalvalue导致内存泄漏的问题,以及为什么普通new对象比如User或HashMap不会引起内存泄漏。首先,我得回忆一下相关知识,确保自己理解正确。 用户的问题集中在ThreadLocal内存泄漏原因,尤其是value部分,同时对比普通的对象创建为什么不泄漏。我需要结合用户提供的引用资料来组织答案。 首先,ThreadLocal内存泄漏问题,通常和ThreadLocalMap的实现有关。根据引用[1],ThreadLocalMap使用哈希算法处理冲突,和HashMap类似,但它的Entry是弱引用。这里可能需要解释弱引用的概念,以及为什么弱引用会导致内存泄漏的可能性。 然后,引用[2]和[4]中的例子说明,当线程没有及时清理ThreadLocal中的值时,大对象可能无法被回收,尤其是线程池中的线程复用,导致旧值残留。这时候需要解释线程生命周期和ThreadLocal值清除的关系。 接下来,用户提到的普通new对象,比如User或HashMap,为什么不会泄漏。这可能涉及到强引用和弱引用的区别。普通对象如果没有被引用,会被GC回收,而ThreadLocal中的Entry的key是弱引用,但value是强引用,导致key被回收后value仍然存在,除非手动remove。 需要分点解释ThreadLocal泄漏的原因: 1. Entry的key是弱引用,容易被回收,导致keynull,但value仍存在。 2. 线程长期存活(如线程池)导致value无法回收。 3. 需要手动remove来清除entry,否则value的强引用链存在。 然后对比普通对象的情况: 1. 普通对象如果没有强引用,GC会自动回收。 2. 普通对象的生命周期由引用链决定,而ThreadLocalvalue的生命周期被ThreadLocalMap的Entry强引用维持,即使业务代码不再使用。 可能还需要提到ThreadLocal的正确使用方式,比如使用后remove,或者使用static的ThreadLocal来避免每个实例创建新的Entry,但这可能不是用户问题的重点。 需要确保引用正确的资料,比如引用[1]提到哈希冲突和Entry结构,引用[3]强调及时清理,引用[4]的例子说明线程复用导致的问题,引用[5]关于HashMap的内存泄漏可能对比,但用户的问题主要是ThreadLocal和普通对象的区别。 总结起来,回答的结构应该是先解释ThreadLocal内存泄漏的原因,分点说明Entry结构、弱引用、强引用和线程生命周期的影响,再对比普通对象的情况,强调强引用和GC回收机制的不同,最后给出预防措施。</think>### ThreadLocalvalue内存泄漏原因与普通对象的区别分析 #### 一、ThreadLocalvalue内存泄漏原因 1. **Entry的弱引用设计缺陷** ThreadLocalMap的Entry使用弱引用持有ThreadLocal对象(即Entry的key),而value仍通过强引用存储。当ThreadLocal实例被回收时,Entry的key变为null,但value仍被Entry强引用保留。若线程长期存活(如线程池场景),且未调用`remove()`清理,value会持续占用内存[^1][^4]。 2. **线程生命周期与对象回收的关联** 线程池中的线程会复用,若未清理ThreadLocal的值,新任务可能通过线程ID复用访问到旧的value,导致本该回收的大对象长期驻留内存[^2][^4]。 3. **强引用链的维持** value的强引用链为:`Thread -> ThreadLocalMap -> Entry -> value`。即使业务代码中不再使用value,只要线程未终止且未清理Entry,value就无法被GC回收[^3][^5]。 #### 二、普通new对象为何不会泄漏 1. **强引用的可控性** 普通对象(如User、HashMap)的生命周期由代码中的强引用直接控制。例如: ```java User user = new User(); // 强引用 user = null; // 取消强引用后,对象可被GC回收 ``` 当强引用被显式置为null或超出作用域时,对象失去引用,GC会直接回收。 2. **无隐藏的强引用链** 普通对象的引用链仅存在于代码逻辑中,没有类似ThreadLocalMap的隐藏结构长期维持引用。例如HashMap的内存泄漏通常是因为未正确管理键值引用(如使用对象作为key后修改其哈希字段),而非设计缺陷。 #### 三、关键区别总结 | 维度 | ThreadLocalvalue | 普通new对象(如User/HashMap) | |---------------------|--------------------------|------------------------------| | **引用类型** | Entry对value是强引用 | 依赖用户代码中的强引用 | | **回收触发条件** | 需手动remove或线程终止 | 强引用消失后自动GC回收 | | **隐藏引用链风险** | 有(ThreadLocalMap结构) | 无 | | **线程池场景风险** | 高(线程复用导致累积) | 低(依赖业务代码管理) | #### 四、预防措施 1. **使用后立即调用remove()** 在ThreadLocal操作完成后(尤其是线程池场景),需显式调用`remove()`断开Entry对value的强引用[^3]。 2. **尽量使用static final修饰** 避免频繁创建ThreadLocal实例,减少Entry数量。 3. **使用带初始化的ThreadLocal** 通过`withInitial()`方法初始化,避免null值干扰判断。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值