李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 左邻右舍
  • 博友圈
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
    • 网站地图
    • 赞助
  • 留言
  • 走心评论
  • 系列文章
  • Now
  • 每日心情
  • 🚇开往
Destiny
自是人生长恨水长东
  1. 首页
  2. 转载
  3. 技术
  4. 正文

ThreadLocal如何解决内存泄漏问题

2024年4月12日 约 2,455 字9 分钟 23点热度 0人点赞 0条评论
本文最后更新于 2024年4月12日,距今已 848 天,其中的信息可能已经发生变化,请注意甄别。

什么是内存泄漏?

不再用到的内存,没有及时释放,就叫做内存泄漏。

对于持续运行的服务进程,必须及时释放内存,否则内存占用率越来越高,轻则影响系统性能,重则导致进程崩溃。

ThreadLocal是怎么造成内存泄露的呢?

如果发生了下面的情况:

  • 如果ThreadLocal是null了,也就是要被GC回收了,
  • 但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。

总之,就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

我们细致的分析一下。

ThreadLocal 有两个引用链

ThreadLocalMap中的Key就是ThreadLocal对象,ThreadLocal 有两个引用链:

  • 一个引用链是栈内存中ThreadLocal引用:
  • 一个引用链是ThreadLocalMap中的Key对它的引用:图片

而对于Value(实际保存的值)来说,它的引用链只有一条,就是从Thread对象引用过来的,如下图:图片

上述过程分析后,就会出现如下的两种情况:

情况1: key的泄漏

情况2: value的泄漏

情况1:key的泄漏

栈上的ThreadLocal Ref引用不再使用了,即当前方法结束处理后,这个对象引用就不再使用了,

那么,ThreadLocal对象因为还有一条引用链存在,如果是强引用的话,这里就会导致ThreadLocal对象无法被回收,可能导致OOM。图片

情况1 的解决方案,使用弱引用解决 。

情况2: value的泄漏

情况2.假设我们使用了线程池,如果Thread对象一直被占用使用中(如在线程池中被重复使用),但是此时我们的ThreadLocalMap(thread 的内部属性)生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。

这就意味着,Value这条引用链就一直存在,那么就会导致ThreadLocalMap无法被JVM回收,可能导致OOM,如上图。

情况2 ,比较严重。还得另想办法。

情况1的解决方案:使用弱引用,解决key的内存泄露

从如下ThreadLocal中内部类Entry代码可知:

Entry类的父类是弱引用WeakReference,ThreadLocal的引用k通过 WeakReference 构造方法传递给了 父类WeakReference的构造方法,

从而,ThreadLocalMap中的Key是ThreadLocal的弱引用,通过弱引用来解决内存泄露问题。

具体的代码如下

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k); // key为弱引用
                value = v;
            }
        }
}

栈内存中的ThreadLocal Ref引用不再使用了,即当当前方法结束处理后,这个key对象引用就不再使用了,

那么,如果这里 不用弱引用而是强引用的话,这里ThreadLocal对象因为还有一条引用链存在,所以就会导致他无法被回收,可能导致OOM。图片

回顾Java中4种引用类型

  1. 强引用(Strong Reference):

    • 这是最常见的引用类型。一个对象具有强引用,垃圾收集器就不会回收它,即使系统内存空间不足。
    • 示例:Object obj = new Object(); 在这里,obj就是new Object()的一个强引用。
  2. 软引用(Soft Reference):

    • 用来描述一些可能还有用但并非必需的对象。在系统将要发生内存溢出异常前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
    • 在Java中,软引用是用来实现内存敏感的高速缓存。
    • 示例:使用java.lang.ref.SoftReference类可以创建软引用。
  3. 弱引用(Weak Reference):

    • 这里讨论ThreadLocalMap中Entry类的重点。
    • 弱引用也是用来描述非必需对象的,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收被弱引用关联的对象。
    • 在Java中,弱引用是用来描述那些非关键的数据,在Java里用java.lang.ref.WeakReference类来表示。
    • 示例:使用java.lang.ref.WeakReference类可以创建弱引用。
  4. 虚引用(Phantom Reference):

    • 一个虚引用关联着的对象,在任何时候都可能被垃圾收集器回收,它不能单独用来获取被引用的对象。虚引用必须和引用队列(ReferenceQueue)联合使用。主要用来跟踪对象被垃圾回收的活动。
    • 虚引用对于一般的应用程序来说意义不大,主要使用在能比较精确控制Java垃圾收集器的高级场景中。
    • 示例:使用java.lang.ref.PhantomReference类可以创建虚引用。

弱引用也是用来描述非必需对象的,它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收被弱引用关联的对象。

至此,key的泄漏问题,JDK已经帮我们顺利解决。 

更复杂的是: 如何解决value内存泄露问题? 

情况2的解决方案:清理策略解决value内存泄露图片

为了解决value内存泄露问题,Java 的 ThreadLocal 实现了两大清理方式:

  • 探测式清理(Proactive Cleanup)
  • 启发式清理(Heuristic Cleanup) 。

源码:value的 探测式清理 :

当线程调用 ThreadLocal的 get()、set() 或 remove()方法时,会探测式的去触发对 ThreadLocalMap 的清理。

此时,ThreadLocalMap 会检查所有键(ThreadLocal 实例),并移除那些已经被垃圾回收的key键及其对应的value 值。

这种清理是主动的,因为它是在每次操作 ThreadLocal 时进行的。

探测式清理(Proactive Cleanup)如何实现的呢?:

从当前节点开始遍历数组,将key等于null的entry置为null,key不等于null则rehash重新分配位置,若重新分配上的位置有元素则往后顺延。

注意:这里把清理的开销放到了get、set操作上,如果get的时候无用Entry(Entry的Key为null)特别多,那这次get相对而言就比较慢了。

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    // 将k=null的entry置为null
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    // k不为null,则rehash从新分配配置
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            // 重新分配后的位置上有元素则往后顺延。
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

源码:value的启发式清理:

在 ThreadLocalMap 的 set() 方法中,有一个阈值(默认为 ThreadLocalMap.Entry 数组长度的 1/4)。

当 ThreadLocalMap 中的 Entry 对象被删除(通过键的弱引用被垃圾回收)并且剩余的 Entry 数量大于这个阈值时,会触发一次启发式清理操作。

这种清理是启发式的,因为它不是每次操作都进行,而是基于一定的条件和概率。

启发式清理(Heuristic Cleanup)如何实现?:

从当前节点开始,进行do-while循环检查清理过期key,结束条件是连续n次未发现过期key就跳出循环,n是经过位运算计算得出的,可以简单理解为数组长度的2的多少次幂次。

private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    // 移除
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

业务主动清理:手动清除解决内存泄露

要知道,ThreadLocal的一个常见问题是内存泄露。

这通常发生在使用线程池的场景中,因为线程池中的线程通常是长期存在的,它们的ThreadLocal变量也不会自动清理,这可能导致内存泄漏。

前面讲了,JDK已经用尽全力去解决了,JDK 用了三个办法,来解决内存泄漏。

尽管有弱引用以及这些清理机制,但最佳实践业务主动清理,

业务上解决这个问题的一个方法是,每当使用完ThreadLocal变量后,显式地调用remove()方法来清除它:

如何业务主动清理?在使用完 ThreadLocal 后显式调用 remove()方法,以确保不再需要的值能够被及时回收,key和value 都同时清理,一锅端。

这样可以避免潜在的内存泄漏问题,并减少垃圾回收的压力。

讲到这里,尼恩团队给大家,用一个大的图总结一下 ThreadLocal的内存泄露与解决方案,具体如下:

图片

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/transport/4096

推荐阅读

  • ThreadPoolExecutor如何实现线程复用及超时销毁
  • Java并发编程之如何保证线程顺序执行
  • 分布式锁-Zookeeper实现分布式锁
  • 分布式锁-Java常用技术方案
  • 分布式、多线程、高并发概念与区别
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA ThreadLocal 分布式 多线程
最后更新:2024年4月12日

岁月同一天 8 月 8 日

回望过去的今天,你在写什么

  • 4 年前 2022年8月8日
    减肥四个月~

    今年四月初开始每天跳绳减肥,到了今天刚好四个月了。从一开始的每天跳绳十分钟到半个小时,再从半个小时到一个小时,最后又稳定…

相关文章
  • Spring WebFlux深度解析:异步非阻塞架构与实战落地指南2025年12月15日
  • Java并发编程之如何保证线程顺序执行2021年5月18日
  • BeanCopier工具类(性能优化工具类)2019年7月10日
  • mybatis-plus-join-boot-starter介绍及用法2025年4月24日
  • Java枚举梳理总结一2019年6月19日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >
1234567891112131415161718192021222324252627282930313233343536373839404142434446474849505152535455575859606162636465666769727476777879808182858687909293949596979899
取消回复
…

文章评论

还没有评论,快来抢沙发吧~

九年没联系的女友突然问我借钱,我开口道:四十万行么?不够了再说话。”前女友很是高兴地回复说:“够用,还是我男朋友有办法啊,你放心,我日后一定还给你,九年多没见了你现在干啥呢,这么有钱?”我呵呵一笑:“没事,不够用您尽管说话,俺现在改做抵押贷款了。”

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
Kratos+ v1.1.14版本更新说明 Spring Boot 指定外部配置文件的方式 Spring Boot 配置加载优先级总结 Claude Fable 5(claude-fable-5)深度详解 如何通过命令查看Java应用内存中对象数量 关于服务的探活端口和业务端口不一致有什么问题
给主题增加了Now、每日心情、年度回顾、岁月同一天、随机漫步等功能AI时代,个人技术博客的出路在哪里?增加了两套复古皮肤-牛皮纸、千禧网页这个域名注册整整十年了,十年时间,真快啊WordPress实现用户评论等级排行榜插件WordPress网站换了个字体,差点儿把样式换崩了
MybatisCodeHelperPro激活 解决kubectl exec -it xxxx-service-bfbd45bb9-ktvzj bash -n bit error: exec [POD] [COMMAND] is not supported anymore. Use exec [POD] -- [COMMAND] instead See 'kubectl exec -h' for help and examples Apollo配置中心中的protalDB的作用是什么 k8s + docker + Jenkins使用Pipeline部署SpringBoot项目时Jenkins错误集锦 彻底搞懂mysql日志系统binlog,redolog,undolog Spring事件驱动深度指南:从单机异步到亿级流量,比MQ更轻的架构神器
最近评论
Huo 发布于 8 小时前(08月07日) 挺有特色的主题,还是感觉 WP 的确是强大
李锋镝 发布于 21 小时前(08月07日) 没理解你想说啥
aboss 发布于 21 小时前(08月07日) 你的后台web-login?
李锋镝 发布于 22 小时前(08月07日) 这个专门的插件实现的功能更好更全,还能对接支付之类的
李锋镝 发布于 22 小时前(08月07日) 自定义登录地址是为了防止大部分机器人通过WP固定登录页面暴力破解用户账号密码
标签聚合
K8s 日常 Claude docker JVM WordPress Redis AI编程 多线程 SQL 架构 MySQL IDEA JAVA SpringBoot Spring AI 数据库 ElasticSearch 分布式
友情链接
  • Blogs·CN
  • 懋和道人
  • 旧时繁华
  • Honesty
  • 拾趣博客导航
  • 搬砖日记
  • 知向前端
  • 临窗旋墨
  • 皮皮社
  • 瓦匠个人小站
  • 志文工作室
  • 韩小韩博客
  • 风渡言
  • 彬红茶日记
  • 老张博客
  • Mr.Sun的博客
  • 韩情脉脉
  • 哥斯拉
  • 林羽凡

COPYRIGHT © 2026 lifengdi.com. ALL RIGHTS RESERVED.

正在博友圈履约中

域名年龄

Theme Kratos+ By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号