Java 中如何正确使用 ThreadLocal 避免内存泄漏?
讲解 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 是否残留。
原文链接:https://www.gj0.com/thread-1805.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。