ThreadLocal源码解读:内存泄露问题分析

引言

大家好,我们又见面了。今天依旧是结合源码为大家分享个人对于 ThreadLocal 的一些理解。今天是第二期,将着重分析 ThreadLocal 内存泄露问题,文章后半篇含重点源码精讲,不容错过。废话不多说,坐稳发车咯!

上期回顾

在上一期,我通过阅读源码的方式带大家学习了 ThreadLocal 常用的 API,并在这个过程中深度剖析了 ThreadLocal 的存储结构。

下面通过我刚刚绘制的一张图来为大家回顾一下上一节所阐述的存储结构。

如果大家对这个存储结构有所疑惑,可以回看第一期《ThreadLocal 源码解读:初识 ThreadLocal》。

引用类型

在 Java 中有四种常用的引用类型,依照引用的强弱排序依次是:强引用、软引用、弱引用、幻引用(虚引用)。

其中强引用就是我们通常所说的引用,所以这里 Java 并没有单独定义一个引用类来表示,并且强引用存在时被引用对象一定不会被垃圾回收器回收。

软引用在 Java 中使用 SoftReference 类表示,被软引用单独引用的对象当系统内存不足的时候会被垃圾回收器所回收,也就是说在发生 OOM 前将会回收软引用对象,试图避免 OOM 的发生。

弱引用在 Java 中使用 WeakReference 类表示,被弱引用单独引用的对象在发生任意垃圾回收时,无论内存是否充足都将会被回收。

幻引用在 Java 中使用 PhantomReference 类表示,是最弱的引用类型,主要用于跟踪对象是否被垃圾回收,并且幻引用的 get 方法永远返回 null。

上述三种引用类均继承 Reference 类,Reference 类通过泛型成员变量 referent 存储引用对象,并提供了 get 方法用于获取引用对象,提供 clear 方法用于清理引用对象。

内存泄露问题剖析

抛出观点

在探究 ThreadLocal 内存泄漏问题之前,我们首先要明确一下,什么是内存泄露?

这里我们直接引用百度百科提供的答案。

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

那么 ThreadLocal 在使用过程中存在泄露问题吗?答案是肯定的,但是要纠正一点,ThreadLocal 的内存泄露问题与 ThreadLocal 对象的弱引用并无关系!这一点在网上可能存在着误导信息,下面将会为大家论证我的观点。

推理验证

首先我们来看一下 Entry 类的定义。

可以看到 Entry 类继承了 WeakReference 类,并且将弱引用的 ThreadLocal 对象作为了 ThreadLocalMap 的键。

查阅过 ThreadLocal 相关博客的小伙伴可能看过下面种说法。

–start–

ThreadLocal 变量如果未被正确清理,可能会导致内存泄露。因为 ThreadLocalMap 的键是 ThreadLocal 对象的弱引用,值是强引用。

当 ThreadLocal 对象不再被外部引用时,ThreadLocalMap 中的键会被垃圾回收,但值仍然存在,导致无法被垃圾回收,从而引发内存泄露。

–end–

在这个过程中的确存在内存泄露问题,但这和 ThreadLocalMap 的 key 设计并无关系,这是编写程序的不严谨导致的问题,在使用完 ThreadLocal 后,没有调用 remove 方法显式移除值。

任何一个 Java 对象都可能因为使用不当导致内存泄漏,比如声明了一个类的对象用作成员变量,但是却从未在代码里使用过这个成员变量(如下图),这也是内存泄漏。

所以并不是因为 ThreadLocalMap 的 key 的弱引用设计,才导致的内存泄露问题。恰恰相反,ThreadLocalMap 的 key 的弱引用设计一定程度上减少了内存泄露的损失。

首先当 ThreadLocalMap 的 key 不再被外部所引用时,ThreadLocal 对象以及通过 ThreadLocal 存储在 ThreadLocalMap 中的值已经无法在其他地方被获取,已经发生了内存泄漏。那么这时候垃圾回收器回收掉 ThreadLocalMap 的 key,恰恰为我们释放了一部分已经泄露的内存。

这时候有人可能会有疑问,那 value 就不管了吗?当然不是!虽然这是开发者 API 使用不当留下的坑,但是设计者也为我们填了这个坑。

注意看 Entry 类的注释,这里我直接为大家翻译出来。

可以看到官方将 key 为 null 的 Entry 对象称之为“陈旧条目”,也就是我上一期文章所说的过时 Entry,并且官方指出这些过时 Entry 可以从 ThreadLocalMap 中删除。

那么不难猜到,ThreadLocal 在设计时一定在某些时机对这些过时 Entry 进行了清理,尽可能的释放泄露的内存。

这里先给出大家结论,然后我们再去论证:ThreadLocal在调用set(),get(),remove()方法的时候,都可能触发清理过时Entry的逻辑。

清理方法源码剖析

expungeStaleEntry 方法

在讨论到 ThreadLocalMap 过时 Entry 清理的问题,就绕不开 ThreadLocalMap 的 expungeStaleEntry 这个方法,见名之意这个方法用于删除过时 Entry。

下面我将采用在源码中添加注释的方式剖析这个方法。

/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
private int expungeStaleEntry(int staleSlot) {
   
   
    // 入参 staleSlot: 待清理位置下标
    
    // 获取 ThreadLocalMap 中的 Entry 数组。
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    // len 为当前 Entry 数组容量。
    int len = tab.length;

    // expunge entry at staleSlot
    // 清除当前 staleSlot 位置的过时 Entry。
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 元素数量减一。
    size--;

    // Rehash until we encounter null
    // 因为 ThreadLocalMap 解决哈希冲突采用的是线性探测法,如将当前下标位置赋值为 null ,但不对后续 Entry
    // 元素进行 rehash 操作,就可能导致存在哈希冲突的后置元素无法被探测到。所以将当前元素清理后需要
    // 对后续元素进行 rehash 操作,直到遇到下一个为 null 的元素。
    ThreadLocal.ThreadLocalMap.Entry e;
    int i;
    // nextIndex 用于向后递增索引 ((i + 1 < len) ? i + 1 : 0)
    for (i = nextIndex(s
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值