⭐ 推荐:社区规则条款 V1.0

Java 中如何正确使用 ThreadLocal 避免内存泄漏?

chinaz
chinaz 归队站长新人站长
发布于 2026-10-13 01:38 ·2 浏览 ·0 回复
内容摘要

讲解 ThreadLocal 内存泄漏的引用链,并给出在 JDK 8 及以上、线程池复用场景下避免内存泄漏的使用规范。

学完这篇你能得到一套可直接套用的 ThreadLocal 使用规范,在 JDK 8 及以上版本中写出不会因线程池复用而导致内存泄漏的代码。

第一步:先搞清楚 ThreadLocal 为什么会泄漏

这一步要弄明白泄漏的引用链,知道问题出在哪儿,后面才知道该防谁。

每个 Thread 对象内部有一个 threadLocals 字段,类型是 ThreadLocal.ThreadLocalMap。这个 Map 的每个 Entry 长这样:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);          // key 是弱引用
        value = v;         // value 是强引用
    }
}

当外部的 ThreadLocal 对象没有强引用时,key 会被 GC 回收变成 null,但 value 仍被 Entry 强引用,而 Entry 又被 Thread 对象引用。线程池里的核心线程存活时间很长,value 就一直回收不掉,这就是泄漏。

注意:ThreadLocalMap 在 get()/set() 时会顺带清理 key 为 null 的 Entry(expungeStaleEntry),但这是“顺手”行为,不能保证及时,更不能依赖它兜底。

第二步:把 ThreadLocal 声明成 private static final

这一步要固定住 ThreadLocal 实例的引用,保证全局只有一个,做完后 key 不会被提前回收。

在类里这样写:

public class DateUtil {
    private static final ThreadLocal<SimpleDateFormat> FMT =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
}

不要写成局部变量或每次 new。如果每次调用都 new 一个 ThreadLocal,Map 里会堆一堆 key 不同的 Entry,泄漏更快。

第三步:用 try-finally 保证 remove()

这一步要确保用完立刻清掉 value,做完后 Entry 的 value 不再被强引用。

public static String format(Date date) {
    try {
        return FMT.get().format(date);
    } finally {
        FMT.remove();
    }
}

remove() 会删除当前线程 Map 中对应的 Entry,value 失去强引用后可被回收。顺序必须是先 get()/set(),再在 finally 里 remove()。

注意:不要只在正常流程末尾写 remove()。一旦中间抛异常,remove() 被跳过,泄漏照样发生。

第四步:线程池任务里的标准写法

这一步要把 ThreadLocal 的读写包在任务内部,做完后线程归还池子时上下文是干净的。

private static final ThreadLocal<User> USER = new ThreadLocal<>();

public void handle(Request req) {
    pool.submit(() -> {
        try {
            USER.set(req.getUser());
            doBusiness();
        } finally {
            USER.remove();
        }
    });
}

如果你用的是 InheritableThreadLocal,要清楚它只在创建子线程时传递一次,线程池复用线程时不会重新传递。需要跨线程池传递上下文时,改用阿里开源的 TransmittableThreadLocal(TTL),配合 TtlExecutors.getTtlExecutorService(pool) 包装线程池。

注意:不要把大对象(比如大 List、字节数组)放进 ThreadLocal 长期持有,即使写了 remove,忘记一次就是几 MB 的泄漏。

第五步:验证到底有没有泄漏

这一步用堆转储确认 ThreadLocalMap 里是否残留 value,做完后能判断代码是否真的干净。

先找到 Java 进程号:

jps -l

再导出堆快照(JDK 8 自带 jcmd):

jcmd <pid> GC.heap_dump /tmp/heap.hprof

用 Eclipse Memory Analyzer(MAT)打开这个 hprof 文件,在菜单 File → Open Heap Dump 选择文件,然后在 Histogram 里输入 java.lang.ThreadLocal$ThreadLocalMap$Entry,右键 → List objects → with incoming references。如果看到大量 Entry 的 key 为 null、value 还指向业务对象,说明有泄漏。

注意:生产环境导堆快照会 STW 暂停,尽量在低峰期做,或先在测试环境复现。

小结

  • ThreadLocal 泄漏的根因:key 弱引用、value 强引用,线程池线程长期存活。
  • ThreadLocal 一律 private static final,不要每次 new。
  • 用完必须 remove(),并放在 finally 里。
  • 线程池任务、跨线程传递场景要额外确认上下文是否清理干净。
  • 用 jcmd + MAT 验证 Entry 的 key 是否为 null、value 是否残留。
版权声明:本文来自 GJ站长论坛《Java 中如何正确使用 ThreadLocal 避免内存泄漏?》
原文链接:https://www.gj0.com/thread-1805.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~