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/8/6 8:50:12 阅读更多 →
进阶指南:掌握NVIDIA Profile Inspector的显卡参数深度调校

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

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

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

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

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

2026/8/6 8:50:12 阅读更多 →

最新新闻

别再瞎降重!✅这款靠谱AI论文软件,才是论文降重的正确打开方式

别再瞎降重!✅这款靠谱AI论文软件,才是论文降重的正确打开方式

写论文最崩溃的瞬间:查重一飘红,改完又超标,降重完逻辑全乱😭 很多同学为了降重,换了无数个AI论文工具,结果踩坑不断!要么机械换词、句子不通顺,要么改完数据出错、专业术语乱改&am…

2026/8/6 9:45:42 阅读更多 →
拯救者笔记本性能调校神器:Lenovo Legion Toolkit完全指南

拯救者笔记本性能调校神器:Lenovo Legion Toolkit完全指南

拯救者笔记本性能调校神器:Lenovo Legion Toolkit完全指南 【免费下载链接】LenovoLegionToolkit Lightweight Lenovo Vantage and Hotkeys replacement for Lenovo Legion laptops. 项目地址: https://gitcode.com/gh_mirrors/le/LenovoLegionToolkit 你是否…

2026/8/6 9:45:42 阅读更多 →
铝件 / 钢件加工打刀缸区分,台湾钰腾不同工况选型思路

铝件 / 钢件加工打刀缸区分,台湾钰腾不同工况选型思路

搞机加工的兄弟应该都有体会:同样一台加工中心,干铝件和干钢件,对打刀缸的要求完全不一样。铝件怕打刀力太大把拉爪顶坏,钢件怕打刀力不够刀松不开。选错了,轻则拉爪轴承提前报废,重则掉刀撞主轴。 今天结合…

2026/8/6 9:45:42 阅读更多 →
常熟服务业 GEO 运营笔记,依托豆顶顶深耕苏州县域市场

常熟服务业 GEO 运营笔记,依托豆顶顶深耕苏州县域市场

先问个问题:你在豆包、DeepSeek里搜过“常熟财税咨询哪家好”或者“常熟ISO认证机构推荐”吗? 一个常熟做财税咨询的朋友试了一下,回来脸色不太好看。他在本地做了八年,服务过几百家企业,结果AI推荐了五家同行&#xf…

2026/8/6 9:45:42 阅读更多 →
智慧树自动刷课插件:3分钟实现高效学习的智能助手终极指南

智慧树自动刷课插件:3分钟实现高效学习的智能助手终极指南

智慧树自动刷课插件:3分钟实现高效学习的智能助手终极指南 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 你是否曾经为了完成智慧树平台的在线课程而不得不…

2026/8/6 9:45:42 阅读更多 →
分布式系统故障放大效应(EM问题)的成因、排查与弹性设计实战

分布式系统故障放大效应(EM问题)的成因、排查与弹性设计实战

1. 从一次深夜告警说起:EM问题到底是什么? 凌晨两点,手机突然震动,监控大屏上一个服务接口的P99延迟曲线像坐了火箭一样直线飙升,紧接着就是一连串的“服务不可用”告警。相信很多负责线上稳定性的同学都经历过这种惊心…

2026/8/6 9:44:41 阅读更多 →

日新闻

深入解析LimboAI C++内核:架构设计与性能优化实战

深入解析LimboAI C++内核:架构设计与性能优化实战

1. 项目概述:为什么我们需要深入LimboAI的C内核?如果你是一名使用Godot引擎的游戏开发者,尤其是对AI行为逻辑有较高要求的项目,那么LimboAI这个名字你大概率不会陌生。它作为Godot 4生态中一个备受瞩目的行为树与状态机插件&#…

2026/8/6 0:00:06 阅读更多 →
Unity 2D游戏敌人AI系统:基于PlayMaker状态机与2D Toolkit的实战开发

Unity 2D游戏敌人AI系统:基于PlayMaker状态机与2D Toolkit的实战开发

1. 项目概述与核心思路大家好,我是老张,一个在游戏开发一线摸爬滚打了十多年的老码农。今天咱们接着聊《空洞骑士》风格2D动作游戏的Demo制作。上一期我们搭好了基础框架,处理了角色移动和碰撞,这一期,我们要让游戏世界…

2026/8/6 0:00:06 阅读更多 →
被动防火门市场前景发展趋势

被动防火门市场前景发展趋势

被动防火门依靠材质结构、密闭构造阻隔烟火蔓延,无需电控启动,是建筑被动消防系统核心构件,行业依托新规管控、城市更新、工业安全升级迎来稳定扩容,整体朝着合规化、专项化、低碳化、智能化方向发展。现阶段 GB12955‑2024 新版国…

2026/8/6 0:00:06 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/5 15:00:43 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/5 13:13:56 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/5 10:20:36 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/5 23:28:39 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/5 21:00:14 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/5 23:46:51 阅读更多 →