来聊聊ThreadLocal内存泄露分析

简介: 来聊聊ThreadLocal内存泄露分析

ThreadLocal存在不存在内存泄漏,趁此机会和大家聊聊ThreadLocal到底存在不存在内存泄漏以及怎么避免。

Thread中的threadLocals属性

一切都要从 Thread 的一个属性 threadLocals 说起,让我们看下这个属性的介绍:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

这个 threadLocals 属性是一个 ThreadLocal 里的静态类ThreadLocalMap ,它是一个 map,并且是由 ThreadLocal 进行维护管理的。

那么这个 threadLocals ,也就是这个 map 里,存的是什么呢?

我们来看 ThreadLocalMap

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

介绍里说,ThreadLocalMap 是一个定制化的 hash map,在 Entry 里,以键值对的形式存储着 ThreadLocal 对象和 value

于是 ThreadthreadLocals 属性和 ThreadLocal 的关系简图就如下所示:

threadLocals.png

注意,这里 Entry 里的 key ,即 ThreadLocal 对象是以弱引用的形式存在的,这将是本文内存泄露分析的重点之一,但这里先不谈,再继续讲讲 ThreadThreadLocal 的关系。

Thread和ThreadLocal的关系

先上一段示例代码:

public class ThreadLocalDemo {
    private static ThreadLocal<Weapon> weaponThreadLocal = new ThreadLocal<Weapon>() {
        @Override
        protected Weapon initialValue() {
            return new Weapon();
        }
    };
    private static class Player extends Thread {
        @Override
        public void run() {
            weaponThreadLocal.get().level += ThreadLocalRandom.current().nextInt(5);
            System.out.println(getName() + " level: " + weaponThreadLocal.get().level);
            weaponThreadLocal.get().combatEff = weaponThreadLocal.get().level * 10;
            System.out.println(getName() + " combatEff: " + weaponThreadLocal.get().combatEff);
        }
    }
    public static void main(String[] args) {
        Player player1 = new Player();
        Player player2 = new Player();
        player1.start();
        player2.start();
    }
    private static class Weapon {
        int level;
        int combatEff;
        public Weapon() {
            level = 1;
            combatEff = 10;
        }
    }
}

在上述代码中,有两个 Player ,他们进行一场游戏,每个人在游戏开始时都会有一把武器 Weapon 。这把武器在游戏开始时对每个人来说是公平的,它的等级(level)和战斗力 (Combat Effectiveness)都是一个固定值(由 ThreadLocal 初始化)。随着游戏的进行,他们的武器等级会升级,战斗力会变强,但升多少级、变强多少就看造化了(由 ThreadLocalRandom 产生随机数)

看看运行之后的结果吧:

Thread-1 level: 3
Thread-0 level: 5
Thread-1 combatEff: 30
Thread-0 combatEff: 50

看来线程0运气更好一点。

好了,上述的例子只是为了接下来的说明做一个铺垫,下面就从上述例子开始谈谈 ThreadThreadLocal 的关系。

一般来说,可以认为 ThreadLocal 解决了线程间共享变量的问题,即 ThreadLocal 为每个线程维护了一个共享变量的副本,多个线程在修改这个变量时(其实是修改自己的变量副本),不存在线程安全问题,效率也很高。所以,在上述例子中,对于一个共享变量,ThreadLocal 提供了两个功能:

  • 统一设置初始值
  • 每个线程对该值的修改互不影响,做到变量隔离

那么乍一看,ThreadThreadLocal 的关系好像是这样:

threadLocal-key-value.png

可是这样是不对的,如果理解成这样,我上面 Thread中的threadLocals属性 那一大段就白说了。

再把那段的内容概括下,Thread 里有 ThreadLocalMap,而ThreadLocalvalue 以键值对的形式存储在 ThreadLocalMap 中,所以 ThreadThreadLocal 的关系应该是这样:

thread-threadLocal.png

当我们调用 ThreadLocalget()set()remove() 操作 Thread  对应value 时,实际上是由 ThreadThreadLocalMap 在操作 ThreadLocal 对应的 value

对应上述的代码示例,如果我们再给每个 Player 新增一个 Life 的共享变量,又多出一个管理 Life 变量的 ThreadLocal,那么它们的示意图就该是这样的:

add-life.png

至此,ThreadThreadLocal 的关系应该说明清楚了,下面就开始分析 ThreadLocal 中存在的内存泄露问题。

ThreadLocal中内存泄露问题分析

要分析 ThreadLocal 中的内存泄露问题,得看一张 ThreadThreadLocal 从内存角度分析的关系图:

memory.png

从上图进行后续分析。

分析一

从上图可知,Thread 对象里的 threadLocals 持有 ThreadLocalMap 对象,Entry 对象。那么当线程执行完毕,线程对象被回收,ThreadLocalMap 也会被回收。由于 Entry 持有 Weapon 对象,即 value 对象的引用,value 对象也会被回收。除了 ThreadLocal 对象,随着线程执行完毕,所有对象都会被回收,皆大欢喜,没有内存泄露。

分析二

若线程还在执行中,而 ThreadLocal 对象引用被置为 null,即现在不需要 ThreadLocal 了,那么其实 Weapon 也失去了意义,照理说是该把 Weapon 对象回收的,那么怎么回收呢?

一旦 ThreadLocal 对象引用被置为 null,那么由于 Entry 对象持有的是 ThreadLocal 对象的弱引用,那么 ThreadLocal 对象就会在下一次 YGC 时被回收。此时,Entry 对象的 key 为空了,value 无法访问到了,怎么回收呢?原来对此情况早有设计,当每次在 get()set()remove()ThreadLocalMap中的值的时候,都会自动将 key 为空的 value 置为空,那么 value 对象也能够被回收了,不存在内存泄露了。

那么内存泄露到底存在于哪里呢?

分析三

在我们使用 ThreadLocal 时,通常是将它作为私有静态变量使用的。如果把 ThreadLocal 作为成员对象使用,那么每个使用的 ThreadLocal 的类都可能创建一个 ThreadLocal 对象,而 ThreadLocal 其实是使用 ThreadLocalMap 对线程和 value 进行管理的,多个 ThreadLocal 对象没有意义,会造成内存浪费。

但另一方面,把 ThreadLocal 作为静态变量使用的话,它就无法被置空了。ThreadLocal 无法被置空,就无法通过触发弱引用机制来回收 ThreadLocal 对象,Entry 里的 key 就不会为空,就无法通过分析二的方法回收 value 对象。

这就是内存泄露的由来了。

总结一下内存泄露的两个条件:

  • ThreadLocal 作为静态变量使用
  • 线程未执行完毕

在此情况下,线程中的 ThreadLocalMap 中的键值对会越堆越多,可能产生内存溢出问题。

解决办法

如果线程还在执行,那么在 ThreadLocal 的使命完成后,调用它的 remove() 方法,该方法会把 Entry 里的 key 置空,就可以回收 value 对象了。(这里 remove() 方法还有待研究),但用就对了。

线程池脏数据分析

再分析一下 ThreadLocal 和线程池一起使用时的脏数据问题。(其实 ThreadLocal 的内存泄露也多数出现在和线程池一起使用的情况)

由以上分析可知,线程执行完毕,线程对象被回收,一切问题都不会存在。

若线程在线程池中复用,且不调用 remove 方法,那么线程在执行完毕一次任务并复用时,从 ThreadLocalMap 中取出来的 value 就是上一次执行任务完毕后的值。这时候,倘若我们的线程在执行每次任务时,没有调用 set() 方法对 value 重新赋值,那么业务逻辑肯定就错了。

解决办法

  • 线程池复用时,在线程的 run() 方法中要调用 ThreadLocalset() 方法对 value 重新赋值
  • 在线程的 run() 方法最后调用 ThreadLocalremove() 方法
相关文章
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
6月前
|
Java
我们来说一说 ThreadLocal 内存泄漏
我是小假 期待与你的下一次相遇 ~
395 5
|
9月前
|
存储 弹性计算 缓存
阿里云服务器ECS经济型、通用算力、计算型、通用和内存型选购指南及使用场景分析
本文详细解析阿里云ECS服务器的经济型、通用算力型、计算型、通用型和内存型实例的区别及适用场景,涵盖性能特点、配置比例与实际应用,助你根据业务需求精准选型,提升资源利用率并降低成本。
568 3
|
5月前
|
设计模式 缓存 Java
【JUC】(4)从JMM内存模型的角度来分析CAS并发性问题
本篇文章将从JMM内存模型的角度来分析CAS并发性问题; 内容包含:介绍JMM、CAS、balking犹豫模式、二次检查锁、指令重排问题
165 1
|
8月前
|
存储 人工智能 自然语言处理
AI代理内存消耗过大?9种优化策略对比分析
在AI代理系统中,多代理协作虽能提升整体准确性,但真正决定性能的关键因素之一是**内存管理**。随着对话深度和长度的增加,内存消耗呈指数级增长,主要源于历史上下文、工具调用记录、数据库查询结果等组件的持续积累。本文深入探讨了从基础到高级的九种内存优化技术,涵盖顺序存储、滑动窗口、摘要型内存、基于检索的系统、内存增强变换器、分层优化、图形化记忆网络、压缩整合策略以及类操作系统内存管理。通过统一框架下的代码实现与性能评估,分析了每种技术的适用场景与局限性,为构建高效、可扩展的AI代理系统提供了系统性的优化路径和技术参考。
531 4
AI代理内存消耗过大?9种优化策略对比分析
|
JavaScript
如何使用内存快照分析工具来分析Node.js应用的内存问题?
需要注意的是,不同的内存快照分析工具可能具有不同的功能和操作方式,在使用时需要根据具体工具的说明和特点进行灵活运用。
655 159
|
存储 Java
课时4:对象内存分析
接下来对对象实例化操作展开初步分析。在整个课程学习中,对象使用环节往往是最棘手的问题所在。
116 4
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
248 2
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
557 1
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
347 5

热门文章

最新文章