ThreadLocal 天天在用,内存泄漏真在我们线上发生了

举报
yd_264617019 发表于 2026/10/22 10:30:04 2026/10/22
【摘要】 一直觉得ThreadLocal内存泄漏是面试题,直到上季度GC日志里老年代出现规律的锯齿,才知道它离生产事故并不远。先说结构:每个Thread内部有一个ThreadLocalMap,Entry的key是弱引用指向ThreadLocal对象,value是强引用。当外部不再持有ThreadLocal时,key会被GC回收变成null,但value还被Entry强引用着,只要线程活着就释放不掉。业...

一直觉得 ThreadLocal 内存泄漏是面试题,直到上季度 GC 日志里老年代出现规律的锯齿,才知道它离生产事故并不远。

先说结构:每个 Thread 内部有一个 ThreadLocalMap,Entry 的 key 是弱引用指向 ThreadLocal 对象,value 是强引用。当外部不再持有 ThreadLocal 时,key 会被 GC 回收变成 null,但 value 还被 Entry 强引用着,只要线程活着就释放不掉。业务代码里通常是 static final 的 ThreadLocal,key 泄漏风险小,真正的雷是忘了 remove:值本身一直挂在长生命周期线程上。

我们的事故就是登录态拦截器往 ThreadLocal 塞了用户上下文,链路上有个分支提前 return,绕过了 finally 里的清理。Tomcat 线程是复用的,几十个线程各自挂着一个旧上下文,一套权限菜单对象 300KB,dump 里肉眼可见 ThreadLocalMap 的 value 全是 PermissionContext。

修复模板很固定:

try {
    UserContext.set(currentUser);
    return doBusiness(request);
} finally {
    // 出口必须清理,无论哪个分支返回
    UserContext.remove();
}

另外两点:线程池场景用 ThreadLocal 传递轨迹 ID 会丢,换阿里的 TransmittableThreadLocal,用 TtlExecutors 包装线程池;怀疑泄漏时用 jcmd PID GC.class_histogram 或直接 MAT 看 ThreadLocalMap 的持有链。规范比技巧有用——我们后来把入口设值、出口清理做成了 AOP 切面,业务代码不再手动碰 ThreadLocal。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。