内存泄漏本质是线程复用下threadlocal存lambda导致强引用外部对象。解法:threadlocal只存无状态轻量数据;行为逻辑改用静态方法引用或无状态lambda;统一拦截线程池任务出口清理。

这个问题本质是线程复用 + 弱引用键 + 强引用值 + Lambda 隐式持有所致的内存泄漏。第三方线程池(如 Tomcat 的 `http-nio-exec`、Spring 的 `@Async` 线程池、或 `Executors.newFixedThreadPool()`)不会自动清理你代码里创建的 ThreadLocal,更不会管你用 Lambda 捕获了什么对象。一旦 Lambda 被提交到线程池执行,它可能长期驻留在 ThreadLocalMap 中,而它捕获的外部对象(比如 `this`、Service 实例、大缓存等)就一直被强引用,无法 GC。
确认是否真由 Lambda 捕获引发
不是所有 Lambda 都危险,关键看它是否:
- 定义在非静态上下文中(如 Spring Bean 内部、Servlet 类中)
- 捕获了实例变量、`this` 或其他长生命周期对象(例如:() -> service.doSomething())
- 被设置进 ThreadLocal(如 contextHolder.set(() -> ...)),且该 ThreadLocal 生命周期超出单次任务
这类 Lambda 编译后等效于匿名内部类,会生成隐式字段 this$0,把整个外层对象钉在堆里。
根本解法:避免在 ThreadLocal 中存 Lambda
ThreadLocal 应只存轻量、无状态、不持有外部引用的数据(如 String、Long、LocalDateTime)。若必须传递行为逻辑,改用以下方式:
- 传入纯函数式接口参数(如
Supplier<string></string>),但确保实现是静态方法引用:MyUtils::generateId—— 静态方法不捕获this - 用工厂方法创建无状态 Lambda:
() -> new HeavyObject().process(),让对象随任务结束自然消亡 - 彻底不用 ThreadLocal 存行为,改用显式参数传递或上下文对象(如将所需数据提前序列化/拷贝后传入 Runnable)
兜底清理:统一拦截线程池任务出口
不能依赖每个业务方写 finally { tl.remove() } —— 第三方组件你改不了源码。可行方案是:
- 继承
ThreadPoolExecutor,重写afterExecute(Runnable r, Throwable t),在里面对所有已知 ThreadLocal 显式调用remove() - 若用 Spring,可通过
@EventListener监听ContextRefreshedEvent后,用AopProxyUtils.ultimateTargetClass()扫描所有 Bean 中的 static ThreadLocal 字段,再注册统一清理钩子 - 对 Tomcat,可写一个
Filter在请求结束时清理;对@Async,可用@Async方法返回Future后手动触发清理(需配合自定义AsyncUncaughtExceptionHandler)
辅助验证与定位
上线前务必验证是否还有残留:
- 用
jmap -dump:format=b,file=heap.hprof <pid></pid>抓堆,MAT 中搜索java.lang.ThreadLocal$ThreadLocalMap$Entry,检查value是否为你的 Lambda 实现类(如MyService$$Lambda$123) - OQL 查询:
SELECT * FROM java.lang.Runnable WHERE toString.toString().contains("MyService"),看是否有未释放的 Lambda 实例 - 启用 JVM 参数
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,观察 Full GC 后老年代是否持续增长











