ThreadLocal 内存泄漏:`Entry` 的 key 都用弱引用了,为什么还会泄漏
前言ThreadLocal是个好东西给每个线程一份独立的变量副本天然线程隔离常用来存用户上下文、事务、SimpleDateFormat这类每线程一份的东西。但它有个著名的坑用不好会内存泄漏。更让人困惑的是很多人知道ThreadLocalMap的Entry用了弱引用来防泄漏于是产生一个疑问——既然都用弱引用了为什么还会泄漏这正是这篇文章要讲清楚的。弱引用确实解决了一半问题但另一半value它管不到而线程池又把这个隐患放大成了实实在在的线上事故。环境说明本文基于 JDK 8。涉及的引用类型、GC 概念属于 Java 内存管理基础。一、先复现一个会让内存慢慢涨的接口设想一个 Web 接口用ThreadLocal缓存一个比较大的上下文对象publicclassUserContextHolder{privatestaticfinalThreadLocalUserContextCONTEXTnewThreadLocal();publicstaticvoidset(UserContextctx){CONTEXT.set(ctx);}publicstaticUserContextget(){returnCONTEXT.get();}// 注意这里没有提供 remove()也没人调用}// 拦截器里每个请求进来就 set 一份publicclassContextInterceptorimplementsHandlerInterceptor{publicbooleanpreHandle(HttpServletRequestreq,...){UserContextctxbuildContext(req);// 假设这个对象不小UserContextHolder.set(ctx);returntrue;}// 请求结束后没有 remove}这段代码功能上完全正常测试也没问题。但把它放到生产环境用 Tomcat 默认的线程池跑一段时间后你会观察到堆内存缓慢但持续地增长老年代越堆越满最终频繁 Full GC 甚至 OOM。问题在于ThreadLocal用完了没有remove()而线程池里的线程一直活着不销毁那些UserContext对象就一直被挂在线程上GC 回收不掉。要理解为什么得先看ThreadLocal的存储结构。二、根因/底层弱引用只保护了 keyvalue 没人管2.1 数据到底存在哪不是存在 ThreadLocal 里第一个反直觉的点ThreadLocal.set(value)的值并不存在ThreadLocal对象里而是存在当前线程身上。每个Thread对象内部有一个字段threadLocals类型是ThreadLocal.ThreadLocalMap。你调用threadLocal.set(value)时实际是以这个ThreadLocal实例为 key、你的value为 value存进了当前线程的那个ThreadLocalMap。Thread线程对象 └─ threadLocals: ThreadLocalMap └─ Entry[] 每个 Entry 是一个 key-value 对 key ThreadLocal 实例弱引用 value 你 set 进去的值强引用这个设计的好处是天然隔离不同线程有各自的ThreadLocalMap互不干扰。2.2 关键Entry的 key 是弱引用value 是强引用ThreadLocalMap里的Entry定义是这样的简化staticclassEntryextendsWeakReferenceThreadLocal?{Objectvalue;Entry(ThreadLocal?k,Objectv){super(k);// keyThreadLocal作为弱引用valuev;// value 是强引用普通字段}}注意这个不对称的设计keyThreadLocal实例是弱引用Entry extends WeakReferenceThreadLocalkey 被弱引用持有。value你 set 的值是强引用value就是个普通字段被Entry强引用着。为什么 key 要用弱引用就是为了防泄漏当外部不再引用这个ThreadLocal时比如ThreadLocal变量被置空或超出作用域弱引用不阻止 GCkey 就能被回收掉Entry的 key 变成null。设计者的本意是好的但问题恰恰出在这个一半弱、一半强上。2.3 泄漏是怎么发生的key 没了value 还在设想这样一条引用链当外部对ThreadLocal的强引用消失后key 是弱引用 →GC 时被回收Entry的 key 变成null但 value 是强引用它的引用链是Thread→ThreadLocalMap→Entry→value。只要线程还活着这条强引用链就一直在value就永远回收不掉。结果就是ThreadLocalMap里出现一堆key 为null、value 还占着内存的僵尸 Entry——这就是内存泄漏。2.4 为什么线程池让问题致命如果是普通线程用完就结束Thread对象被回收它的ThreadLocalMap连同里面所有Entry、value 一起被回收泄漏也就自愈了——所以短生命周期的线程问题不明显。但线程池里的线程是复用的、长期存活的。一个线程处理完请求 A不会销毁而是回到池里等着处理请求 B、C、D……它的ThreadLocalMap一直存在。于是每个请求set一个UserContext用完不remove线程不死ThreadLocalMap不释放僵尸 Entry或旧 value越积越多内存持续增长最终 OOM。“ThreadLocal 线程池 忘记 remove” 是内存泄漏的黄金三角。这也是为什么这个坑在 Web 应用Tomcat 线程池里特别常见。JDK 其实做了点补救ThreadLocalMap在set/get/remove时会顺带清理一些 key 为null的僵尸 Entry探测式清理。但这个清理是碰运气的、不彻底的绝不能依赖它。根治办法只有一个手动remove。三、正解用完一定remove()最好放在finally里根治方案非常简单每次用完ThreadLocal显式调用remove()。remove()会把当前线程ThreadLocalMap里对应的整个Entrykey 和 value都删掉斩断强引用链。关键是要保证remove()一定被执行所以放在finally里publicbooleanpreHandle(HttpServletRequestreq,...){UserContextHolder.set(buildContext(req));returntrue;}// 在请求结束的回调里 removeSpring 的 afterCompletionpublicvoidafterCompletion(HttpServletRequestreq,...){UserContextHolder.remove();// ✓ 请求结束清理}或者在业务代码里用标准的try-finally包裹try{UserContextHolder.set(ctx);doBusiness();}finally{UserContextHolder.remove();// ✓ 无论是否异常都清理}finally里做清理正是它的正确用法——可参考上一篇《try-finally 里的 return》。几个补充实践拦截器/过滤器场景在afterCompletion或finally里统一remove别依赖 JDK 的探测式清理。把ThreadLocal声明为static final让它跟随类存在避免它被意外回收其实反而是key 不该被过早回收也便于统一管理。注意这和防泄漏不矛盾——防泄漏靠的是remove不是让 key 被回收。父子线程传递用InheritableThreadLocal但线程池下要谨慎线程复用会导致继承的值错乱阿里的TransmittableThreadLocalTTL是更完善的方案。四、常见误区与面试高频问答QEntry的 key 用了弱引用不就是为了防泄漏吗为什么还漏弱引用只解决了keyThreadLocal 实例的回收让没人引用的ThreadLocal能被 GC。但value 是强引用它通过Thread → ThreadLocalMap → Entry → value这条链被线程强引用着只要线程活着就回收不掉。弱引用防了 key防不了 value——这才是泄漏的根源。Q那 key 为什么不干脆也用强引用或者 value 也用弱引用key 用强引用会更糟ThreadLocalMap会强引用ThreadLocal导致ThreadLocal实例本身也回收不掉泄漏更严重。value 用弱引用又不行value 通常没有其他强引用一 GC 就没了ThreadLocal就存不住值了。所以现在这个key 弱、value 强是权衡后的设计代价就是需要你手动remove。QJDK 不是会自动清理 null key 的 Entry 吗会但不可靠。set/get/remove时会触发探测式/启发式清理顺路清掉一些 key 为null的 Entry。但它只清理碰到的部分槽位不保证全清更不会主动触发。如果后续不再调用这个ThreadLocal的方法僵尸 Entry 就一直留着。不能依赖它必须手动remove。Q为什么普通线程没事线程池才严重普通线程执行完就销毁Thread及其ThreadLocalMap整个被回收泄漏自动消失。线程池的线程长期复用、不销毁ThreadLocalMap一直存在不remove的话 value 越积越多泄漏就暴露了。Qremove()和set(null)一样吗不一样。set(null)只是把 value 设为nullEntry本身key 和这个 null value还留在 map 里是半清理。remove()会把整个Entry从 map 中删除才是彻底清理。要remove()。总结“ThreadLocal 的 key 是弱引用为什么还泄漏”答案在那个不对称的设计里ThreadLocal的值存在线程的ThreadLocalMap里Entry的keyThreadLocal是弱引用、value你的值是强引用。弱引用让没人用的 key 能被 GC 回收key 变null但value 仍被Thread → ThreadLocalMap → Entry → value强引用链拴着只要线程活着就回收不掉形成key 为 null、value 常驻的僵尸 Entry。线程池里线程长期复用、不销毁把这个隐患放大成持续的内存泄漏直至 OOM。根治办法只有一个用完remove()并放在finally/afterCompletion里确保执行。别指望 JDK 的探测式清理。一句话记忆弱引用只保护 keyvalue 是强引用、被活着的线程拴着回收不掉ThreadLocal 线程池 忘记 remove 内存泄漏用完必须remove()。

相关新闻

C++深拷贝与浅拷贝:从原理到实践,彻底掌握对象拷贝机制

C++深拷贝与浅拷贝:从原理到实践,彻底掌握对象拷贝机制

1. 项目概述:为什么C程序员必须搞懂拷贝 在C的世界里,拷贝操作无处不在。当你把一个对象赋值给另一个对象,或者将一个对象作为参数传递给函数时,拷贝就发生了。对于很多刚入门的开发者,甚至一些有经验的程序员&#xf…

2026/10/14 12:09:48 阅读更多 →
进阶指南:掌握NVIDIA Profile Inspector的显卡参数深度调校

进阶指南:掌握NVIDIA Profile Inspector的显卡参数深度调校

进阶指南:掌握NVIDIA Profile Inspector的显卡参数深度调校 【免费下载链接】nvidiaProfileInspector 项目地址: https://gitcode.com/gh_mirrors/nv/nvidiaProfileInspector NVIDIA Profile Inspector是一款强大的显卡配置工具,专门用于访问和修…

2026/10/14 12:07:39 阅读更多 →
C++项目整合物理引擎:从Bullet/PhysX/Box2D选型到实战优化

C++项目整合物理引擎:从Bullet/PhysX/Box2D选型到实战优化

1. 项目概述:为什么要在C项目中整合物理引擎? 如果你正在用C开发游戏、仿真软件,或者任何需要模拟现实世界物体运动的程序,那么迟早会碰到一个问题:如何让那些方块、小球或者角色,像真实世界一样下落、碰撞…

2026/10/13 22:30:57 阅读更多 →

最新新闻

AI读不懂你的技术文档?一场Rspress文档改造实录

AI读不懂你的技术文档?一场Rspress文档改造实录

前阵子我给自己长期维护的一个基于 Rspress 搭建的组件库文档站做了一次 AI 可读性测试,测试代号叫 Rspress-20。名字很直白:我从用户真实提问中筛出了 20 个高频问题,比如“某组件支持哪几种尺寸”“某个配置项的默认值是什么”“为什么某个…

2026/10/14 12:10:13 阅读更多 →
Spring源码编译踩坑指南:JDK、Gradle与依赖配置全解析

Spring源码编译踩坑指南:JDK、Gradle与依赖配置全解析

刚拿到Spring源码准备自己编译一份的时候,我其实是有点膨胀的。觉得无非就是clone代码、装个JDK、跑个Gradle,能有多难?结果一整天搭进去,光是在“编译不通过”和“下载依赖失败”之间来回折腾就花了大半时间。后来冷静下来&#…

2026/10/14 12:10:13 阅读更多 →
行波故障诊断原理与Simulink仿真复现实战指南

行波故障诊断原理与Simulink仿真复现实战指南

行波故障诊断,在输电线路保护领域算是一个“老资格”方向了。跟传统的阻抗法测距比起来,行波法不受过渡电阻、系统阻抗、线路不平衡等因素影响,定位精度高出一个数量级,所以这些年论文里、工程里讨论得都很多。最近我复现了一篇基…

2026/10/14 12:10:13 阅读更多 →
AD9834驱动代码详解:DDS信号源寄存器配置与SPI时序实现

AD9834驱动代码详解:DDS信号源寄存器配置与SPI时序实现

简介:面向嵌入式开发者的AD9834驱动代码包,专为需要快速生成高精度正弦波、方波与三角波的测试测量、通信系统和教育实验等场景设计,目标芯片为ADI的DDS数字频率合成器。压缩包仅7KB,共4个文件,包含2个C源文件与2个头文…

2026/10/14 12:10:13 阅读更多 →
C++数组从声明到越界:核心机制与避坑指南

C++数组从声明到越界:核心机制与避坑指南

1. 为什么数组值得单独拎出来讲C 学到数组这一章,很多人会觉得“这不就是一堆相同类型的数据放一起吗”,然后翻两页就过去了。但我带过的新人里,十个有八个在数组上栽过跟头,而且栽的方式五花八门:有人把数组长度写成变…

2026/10/14 12:10:13 阅读更多 →
从二进制到可编辑网格:owmdl模型在Blender中的导入解析

从二进制到可编辑网格:owmdl模型在Blender中的导入解析

简介:基于Blender的《守望先锋》owmdl模型导入插件源码包,主要面向需要在Blender中查看和编辑这一专有游戏格式的玩家、三维建模学习者和游戏资产研究者。插件通过io_scene_owm扩展Blender的导入能力,可解析模型网格、纹理、骨骼与动画数据&a…

2026/10/14 12:09:12 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →