threadlocal 内存泄露之我见
threadlocal 内存泄露之我见#
当heap区中的threadLocal对象(假设为A),在外界没有强引用的情况下,即:
只有线程的threadlocal map中的某一个entry的key,维持着A的weakReference(图中虚线即是)时,这时候,只要一进行gc,那 A 就被回收了。
A回收后,entry变成了如下的样子:
key: null(本来是一个weakReference,执行A对象的,现在A被回收了,这里也变成null)
value:图中的那个最右侧的My 50M value,还在。
这个value所在的entry,已经没法有什么办法去访问了,即:没有办法来访问这个value了,所以,这应该才是内存泄露了。
这种情况下,ThreadLocal类中,有一定的补救措施,但不是很强力,会有一定几率清理到这种entry(key为null的),但不是一定会。
如果,外界一直持有对A对象的强引用,比如:
-
定义为static变量
public abstract class UserReqContextHolder { /** * 当前登录用户的信息 */ private static final ThreadLocal<UserLoginRespVo> currentLoginUserInfo = new NamedInheritableThreadLocal<>("currentLoginUserInfo"); }
每次在filter中,就如下使用:
/** * 设置为线程变量 */ UserReqContextHolder.set(userInfoByToken); try { chain.doFilter(request,response); } finally { UserReqContextHolder.reset(); }
-
定义为全局单例对象的实例field
org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory /** * The name of the currently created bean, for implicit dependency registration * on getBean etc invocations triggered from a user-specified Supplier callback. */ private final NamedThreadLocal<String> currentlyCreatedBean = new NamedThreadLocal<>("Currently created bean");
这个实例field,在AbstractAutowireCapableBeanFactory类中,这个类差不多就是spring的applicationContext的beanFactory,是单例的。
-
在spring aop 中
org.springframework.aop.framework.AopContext#currentProxy /** * ThreadLocal holder for AOP proxy associated with this thread. * Will contain {@code null} unless the "exposeProxy" property on * the controlling proxy configuration has been set to "true". * @see ProxyConfig#setExposeProxy */ private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");
这个对象,保存了当前的代理对象。可以解决如下问题:
可以看到,这里的也是定义为static 类型的。
在上述的情形中,可以用下图来表示,这个ThreadLocal对象,除了我们自己在上面维护了强引用之外,剩下的,就是Thread->ThreadLocalMap->Entry->Key(类型为WeakReference)-> ThreadLocal对象。
所以,就是这两个引用,1个weak,1个强引用。
这种情况下,既然有强引用,那就不可能被gc回收了,上述这几种情况下,这个entry、entry中的key和value、以及ThreadLocal对象本身,都不会被回收。
这种情况下,我觉得也不能算内存泄露,因为线程里,只会多这么几个对象,不会一直涨。如果觉得这样是泄露了,可以尽量地去调用remove操作来清理掉该entry。
这样,这个弱引用就切断了,对value的引用也切断了。如下:
所以,remove操作,只是相当于清除了线程的threadlocalMap中的数据。
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set(s);
threadLocal.remove();
执行了remove后,debug时,看到的信息如下:
而执行完上面的第21行后:
threadlocal.set("kkk");
执行了上面这行之后,可以看到线程ThreadLocal的调试信息如下:
为什么threadlocalMap的entry中,key是weakReference,value不是?#
简单的hashmap来做,有什么问题#
本身,是可以使用一个简单的hashmap来做的;但是,考虑这样一个问题,假设目前,threadlocal对象A,有两个引用执行它,一个是外部我们自己维护的,一个是由threadlocalMap的entry中的key指向的。
如果我们外部维护的引用,已经不再指向A了;但是,因为entry还指向它,这时候,就会回收不了这个A。
所以,jdk选择将其封装为weakReference,只要我们外部不再执行A,则可以保证A被回收掉。
为什么value不是weakReference#
因为我们还需要通过key去找到value,如果value弄成WeakReference,那岂不是经过一次gc后,就取不到value了吗?
所以,value不能是weakReference的。
tomcat中关于weakReference#
/**
* References to class loaders are weak references, so that they can be
* garbage collected when nobody else is using them. The ResourceBundle
* class has no reason to keep class loaders alive.
*/
private static class LoaderReference extends WeakReference<ClassLoader>
implements CacheKeyReference {
private CacheKey cacheKey;
}
其他#
可以查看项目中WeakReference的子类,还挺多的。
相关源码#
package com.lkl.hystrixdemo;
import java.lang.reflect.Field;
public class Test {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(()->test("abc",false));
t.start();
t.join();
System.out.println("--gc后--");
Thread t2 = new Thread(() -> test("def", true));
t2.start();
t2.join();
}
private static void test(String s,boolean isGC) {
try {
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set(s);
threadLocal.remove();
threadLocal.set("kkk");
// new ThreadLocal<>().set(s);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(t);
Class<?> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
什么情况下,threadlocal变量会只有threadlocal中的map去引用#
当在方法内部,作为局部变量去生成时,方法出栈了,也就没有引用了。
也就只剩下threadlocamap中的引用了。
此时就会出现所谓的threadlocal内存泄露。
相关参考博文:
面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端
2018-05-10 Jenkins踩坑系列--你试过linux主机ssh登录windows,启动java进程吗,来试试吧