李锋镝的博客

  • 首页
  • 时间轴
  • 留言
  • 插件
  • 左邻右舍
  • 关于我
    • 关于我
    • 另一个网站
  • 知识库
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 转载
  3. 技术
  4. 正文

ThreadLocal如何解决内存泄漏问题

2024年4月12日 1018点热度 0人点赞 0条评论

什么是内存泄漏?

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

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

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/archives/transport/4096

相关文章

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

李锋镝

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

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

天若有情天亦老,人间正道是沧桑。

最新 热点 随机
最新 热点 随机
SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports 应用型负载均衡(ALB)和网络型负载均衡(NLB)区别 什么是Helm? TransmittableThreadLocal介绍与使用 ReentrantLock深度解析 RedisTemplate和Redisson的区别
玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?什么是Helm?2024年11月1号 农历十月初一别再背线程池的七大参数了,现在面试官都这么问URL地址末尾加不加“/”有什么区别
ConcurrentHashMap常用方法源码解析(jdk1.8) Markdown常用语法 IDEA下载源码报:Cannot connect to the Maven process. Try again later. 新网站 一文详解微服务架构 Java浮点数运算
标签聚合
面试 文学 架构 Spring 日常 SpringBoot JAVA JVM K8s 设计模式 分布式 教程 IDEA docker SQL Redis ElasticSearch 数据库 MySQL 多线程
友情链接
  • i架构
  • 临窗旋墨
  • 博友圈
  • 博客录
  • 博客星球
  • 哥斯拉
  • 志文工作室
  • 搬砖日记
  • 旋律的博客
  • 旧时繁华
  • 林羽凡
  • 知向前端
  • 蜗牛工作室
  • 集博栈
  • 韩小韩博客
  • 風の声音

COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Dylan

津ICP备2024022503号-3