ThreadLocal内存泄漏排查需聚焦key为null而value仍存活的“幽灵Entry”,通过堆快照(jmap+MAT)、线程状态(jstack)与运行时验证(日志/afterExecute)三者交叉定位,重点检查长期复用线程中Filter/Interceptor/AOP等set但未remove的场景。

排查 ThreadLocal 内存泄漏,核心是抓住“key 为 null、value 仍存活”这一典型特征,在线程长期复用的场景下定位残留数据。不靠猜,靠证据——堆快照、运行时状态、代码路径三者交叉验证。
看堆里有没有“幽灵 Entry”
ThreadLocal 泄漏最直观的表现就是大量 ThreadLocalMap$Entry 对象中 key == null,但 value 指向业务大对象(如 UserContext、TraceInfo、byte[] 等)。操作步骤如下:
- 用
jmap -dump:format=b,file=heap.hprof <pid>抓取堆转储 - 用 Eclipse MAT 打开,运行 Leak Suspects Report,重点关注 “ThreadLocalMap → Entry → value” 链路
- 在 Dominator Tree 中筛选
java.lang.Thread,逐个展开其threadLocals字段,检查table数组中是否大量存在 key 为 null 的 Entry - 对可疑的 value 类型右键 → Path to GC Roots,确认它是否被某个长期存活线程(如 Tomcat exec-*、ForkJoinPool-worker)直接强引用
盯住线程生命周期和复用痕迹
短期线程泄漏不明显;真正危险的是那些跑了数小时甚至数天、还反复处理请求的线程。
- 用
jstack <pid>查看线程名和状态,重点关注命名含exec-、pool-、commonPool的线程 - 结合应用日志或 Micrometer 监控,确认某线程是否连续处理了数百/数千个任务却未重启
- 回溯这些线程执行过的调用链:Filter、Interceptor、AOP 切面、Runnable/Callable 提交点——这些地方最常 set 但漏 remove
运行时主动验证是否真有残留
别等 OOM,现场加诊断逻辑快速定位漏删位置:
立即学习“Java免费学习笔记(深入)”;
- 在疑似 set 前加日志:
if (tl.get() != null) log.warn("TL still has value before set: {}", tl.get().getClass()) - 在线程池中重写
afterExecute(),统一打印tl.get() != null的情况 - 对静态 ThreadLocal 尤其警惕:若声明为非 static,每个类加载器都可能持有一份,泄漏风险倍增
用反射辅助诊断(仅限调试与工具)
生产环境禁用,但开发排查时可临时用反射读取 ThreadLocalMap 内部 table,检查 stale entry:
- 通过
Thread.currentThread().getClass().getDeclaredField("threadLocals")获取 map 实例 - 再反射获取
ThreadLocalMap.class.getDeclaredField("table"),转为Object[]后遍历 - 对每个非空元素做
instanceof ThreadLocalMap.Entry校验,再判断entry.get() == null && entry.value != null - 注意:JDK 版本差异可能导致字段名变化;并发修改中读取可能看到不一致快照


















