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变量

    Copy
    public abstract class UserReqContextHolder { /** * 当前登录用户的信息 */ private static final ThreadLocal<UserLoginRespVo> currentLoginUserInfo = new NamedInheritableThreadLocal<>("currentLoginUserInfo"); }

    每次在filter中,就如下使用:

    Copy
    /** * 设置为线程变量 */ UserReqContextHolder.set(userInfoByToken); try { chain.doFilter(request,response); } finally { UserReqContextHolder.reset(); }
  • 定义为全局单例对象的实例field

    Copy
    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 中

    Copy
    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中的数据。

Copy
ThreadLocal<Object> threadLocal = new ThreadLocal<>(); threadLocal.set(s); threadLocal.remove();

执行了remove后,debug时,看到的信息如下:

而执行完上面的第21行后:

Copy
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#

Copy
/** * 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的子类,还挺多的。

相关源码#

Copy
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 @   三国梦回  阅读(593)  评论(0编辑  收藏  举报
编辑推荐:
· 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进程吗,来试试吧
点击右上角即可分享
微信分享提示
CONTENTS