李锋镝的博客

  • 首页
  • 时间轴
  • 说说
  • 读书观影
  • 系列
  • AI工具集
  • 论坛
  • 友圈
    • 友链
    • 朋友圈
    • 游客中心
  • 留言
    • 留言
    • 走心评论
  • 关于
    • 关于本站
    • Now
    • 每日心情
    • 网站地图
    • 网站统计
    • 赞助
  • 星球穿梭
  • 🚇开往
静亦定 动亦定
惟坚韧者始能遂其志
  1. 首页
  2. 转载
  3. 技术
  4. 正文

ThreadLocal如何解决内存泄漏问题

2024年4月12日 243 0 0
本文最后更新于 2024年4月12日,距今已 938 天,其中的信息可能已经发生变化,请注意甄别。

什么是内存泄漏?

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

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

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的内存泄露与解决方案,具体如下:

图片

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
分享到

ThreadLocal如何解决内存泄漏问题

也可使用浏览器菜单中的「分享」功能

微信扫一扫分享

标签: JAVA ThreadLocal 分布式 多线程
最后更新:2024年4月12日

岁月同一天 11 月 7 日

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

  • AI
    1 年前 2025年11月7日
    DeepSeek本地部署全攻略:从环境搭建到高级应用,打造专属 AI 助手

    DeepSeek 作为国产开源大模型的佼佼者,凭借出色的推理能力、代码生成效率和中文支持,成为很多开发者本地部署的首选。…

相关文章
  • 从零开始入门 K8s | Kubernetes 网络概念及策略控制2019年10月17日
  • Java简介2022年7月15日
  • Java布尔运算2022年7月18日
  • JAVA设计模式-工厂方法模式2020年2月3日
  • 了解一下Spring中用了哪些设计模式2019年10月20日

李锋镝

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

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

文章评论

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

一女子请道士驱鬼,说道,“大师!最近实在是奇怪,先是上楼梯,那木头楼梯竟然就硬生生粉碎了,后来我又坐到椅子上,椅子竟然也折了!最恐怖的是,晚上睡觉的时候,床竟然也塌了!请大师救救我!我好害怕!”
道士听罢,掏出桃木剑,舞动一通,剑指女子,大喝:“你该减肥了!”

听点儿音乐吧 朋友~
文章目录
最新 热点 随机
最新 热点 随机
给服务器加了WAF 又一个逆天改命的人:小小徐 AI的生成能力等于生产力吗? NGINX严重漏洞(CVE-2026-15176、CVE-2026-93479、CVE-2026-97205) Kratos-plus v1.1.27更新说明 php2shell远程执行漏洞修复(CVE-2026-109618 和 CVE-2026-109619)
给主题增加了游客中心功能心气没了,干什么都不得劲我觉得是时候公布我的导航网站了给服务器加了WAF游本昌去世关于主题加载速度优化的一点儿小演进
这个域名注册整整十年了,十年时间,真快啊 【收藏】Java问题排查相关工具工具箱 在 Visual Studio 中生成和清理项目和解决方案 Mac版 SpringBoot 中内置的 49 个常用工具类 妹妹的画【2019.09.26】 php2shell远程执行漏洞修复(CVE-2026-109618 和 CVE-2026-109619)
最近评论
fairy 发布于 5 小时前(11月06日) 机器人快来我这里吧!
李锋镝 发布于 6 小时前(11月06日) 都是机器人,防不住
李锋镝 发布于 6 小时前(11月06日) 是的,太烦了
李锋镝 发布于 6 小时前(11月06日) 可以同一台服务器
fairy 发布于 7 小时前(11月06日) 这流量,好厉害啊。我一年都没那么多
标签聚合
架构 AI MySQL K8s Redis Theme 多线程 JAVA SpringBoot WordPress 日常 SQL IDEA 数据库 Claude AI编程 ElasticSearch 分布式 Spring JVM
友情链接
  • Honesty
  • 知向前端
  • 志文工作室
  • 老张博客
  • sssr7844的博客
  • 韩情脉脉
  • 皮皮社
  • lijie blog
  • Mr.Sun的博客
  • 韩小韩博客
  • 哥斯拉
  • 彬红茶日记
  • 懋和道人
  • 瓦匠个人小站
  • Serendipity
  • 搬砖日记
  • 九仞之行
  • 林羽凡
  • 刘永博客

COPYRIGHT © 2016-2026 lifengdi.com. ALL RIGHTS RESERVED.

lifengdi.com 十年之约虫洞

Domain age badge for lifengdi.com

Theme Kratos-plus By Dylan Li

津ICP备2024022503号-3

京公网安备11011502039375号