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");
    

    这个对象,保存了当前的代理对象。可以解决如下问题:

    SpringAOP 失效解决方案

    可以看到,这里的也是定义为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)

posted @ 2020-05-10 18:19  三国梦回  阅读(591)  评论(0编辑  收藏  举报