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内存泄露。
相关参考博文: