写点什么

ThreadLocal 内存泄漏分析与解决方案 (1),linux 文件系统原理

  • 2021 年 11 月 10 日
  • 本文字数:5941 字

    阅读完需:约 19 分钟

如果对输入参数能够理解的话,那么 cleanSomeSlots 方法搜索基本上清除了,但是全部搞定还需要掌握 expungeStaleEntry 方法,当在搜索过程中遇到了脏 entry 的话就会调用该方法去清理掉脏 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) {


Entry[] tab = table;


int len = tab.length;


//清除当前脏 entry


// expunge entry at staleSlot


tab[staleSlot].value = null;


tab[staleSlot] = null;


size--;


// Rehash until we encounter null


Entry e;


int i;


//2.往后环形继续查找,直到遇到 table[i]==null 时结束


for (i = nextIndex(staleSlot, len);


(e = tab[i]) != null;


i = nextIndex(i, len)) {


ThreadLocal<?> k = e.get();


//3. 如果在向后搜索过程中再次遇到脏 entry,同样将其清理掉


if (k == null) {


e.value = null;


tab[i] = null;


size--;


} else {


//处理 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;


}


该方法逻辑请看注释(第 1,2,3 步),主要做了这么几件事情:


  1. 清理当前脏 entry,即将其 value 引用置为 null,并且将 table[staleSlot]也置为 null。value 置为 null 后该 value 域变为不可达,在下一次 gc 的时候就会被回收掉,同时 table[staleSlot]为 null 后以便于存放新的 entry;

  2. 从当前 staleSlot 位置向后环形(nextIndex)继续搜索,直到遇到哈希桶(tab[i])为 null 的时候退出;

  3. 若在搜索过程再次遇到脏 entry,继续将其清除。


也就是说该方法,清理掉当前脏 entry 后,并没有闲下来继续向后搜索,若再次遇到脏 entry 继续将其清理,直到哈希桶(table[i])为 null 时退出。因此方法执行完的结果为 从当前脏 entry(staleSlot)位到返回的 i 位,这中间所有的 entry 不是脏 entry。为什么是遇到 null 退出呢?原因是存在脏 entry 的前提条件是 当前哈希桶(table[i])不为 null,只是该 entry 的 key 域为 null。如果遇到哈希桶为 null,很显然它连成为脏 entry 的前提条件都不具备。


现在对 cleanSomeSlot 方法做一下总结,其方法执行示意图如下:



如图所示,cleanSomeSlot 方法主要有这样几点:


  1. 从当前位置 i 处(位于 i 处的 entry 一定不是脏 entry)为起点在初始小范围(log2(n),n 为哈希表已插入 entry 的个数 size)开始向后搜索脏 entry,若在整个搜索过程没有脏 entry,方法结束退出

  2. 如果在搜索过程中遇到脏 entryt 通过 expungeStaleEntry 方法清理掉当前脏 entry,并且该方法会返回下一个哈希桶(table[i])为 null 的索引位置为 i。这时重新令搜索起点为索引位置 i,n 为哈希表的长度 len,再次扩大搜索范围为 log2(n’)继续搜索。


下面,以一个例子更清晰的来说一下,假设当前 table 数组的情况如下图。



  1. 如图当前 n 等于 hash 表的 size 即 n=10,i=1,在第一趟搜索过程中通过 nextIndex,i 指向了索引为 2 的位置,此时 table[2]为 null,说明第一趟未发现脏 entry,则第一趟结束进行第二趟的搜索。

  2. 第二趟所搜先通过 nextIndex 方法,索引由 2 的位置变成了 i=3,当前 table[3]!=null 但是该 entry 的 key 为 null,说明找到了一个脏 entry,先将 n 置为哈希表的长度 len,然后继续调用 expungeStaleEntry 方法,该方法会将当前索引为 3 的脏 entry 给清除掉(令 value 为 null,并且 table[3]也为 null),但是该方法可不想偷懒,它会继续往后环形搜索,往后会发现索引为 4,5 的位置的 entry 同样为脏 entry,索引为 6 的位置的 entry 不是脏 entry 保持不变,直至 i=7 的时候此处 table[7]位 null,该方法就以 i=7 返回。至此,第二趟搜索结束;

  3. 由于在第二趟搜索中发现脏 entry,n 增大为数组的长度 len,因此扩大搜索范围(增大循环次数)继续向后环形搜索;

  4. 直到在整个搜索范围里都未发现脏 entry,cleanSomeSlot 方法执行结束退出。

replaceStaleEntry 方法

先来看 replaceStaleEntry 方法,该方法源码为:


/*


  • @param key the key

  • @param value the value to be associated with key

  • @param staleSlot index of the first stale entry encounter


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


ed while



    */


    private void replaceStaleEntry(ThreadLocal<?> key, Object value,


    int staleSlot) {


    Entry[] tab = table;


    int len = tab.length;


    Entry e;


    // Back up to check for prior stale entry in current run.


    // We clean out whole runs at a time to avoid continual


    // incremental rehashing due to garbage collector freeing


    // up refs in bunches (i.e., whenever the collector runs).


    //向前找到第一个脏 entry


    int slotToExpunge = staleSlot;


    for (int i = prevIndex(staleSlot, len);


    (e = tab[i]) != null;


    i = prevIndex(i, len))


    if (e.get() == null)


    slotToExpunge = i;//1.


    // Find either the key or trailing null slot of run, whichever


    // occurs first


    for (int i = nextIndex(staleSlot, len);


    (e = tab[i]) != null;


    i = nextIndex(i, len)) {


    ThreadLocal<?> k = e.get();


    // If we find key, then we need to swap it


    // with the stale entry to maintain hash table order.


    // The newly stale slot, or any other stale slot


    // encountered above it, can then be sent to expungeStaleEntry


    // to remove or rehash all of the other entries in run.


    if (k == key) {


    //如果在向后环形查找过程中发现 key 相同的 entry 就覆盖并且和脏 entry 进行交换


    e.value = value;//2.


    tab[i] = tab[staleSlot];//3.


    tab[staleSlot] = e;//4.


    // Start expunge at preceding stale entry if it exists


    //如果在查找过程中还未发现脏 entry,那么就以当前位置作为 cleanSomeSlots


    //的起点


    if (slotToExpunge == staleSlot)


    slotToExpunge = i;//5.


    //搜索脏 entry 并进行清理


    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//6.


    return;


    }


    // If we didn't find stale entry on backward scan, the


    // first stale entry seen while scanning for key is the


    // first still present in the run.


    //如果向前未搜索到脏 entry,则在查找过程遇到脏 entry 的话,后面就以此时这个位置


    //作为起点执行 cleanSomeSlots


    if (k == null && slotToExpunge == staleSlot)


    slotToExpunge = i;//7.


    }


    // If key not found, put new entry in stale slot


    //如果在查找过程中没有找到可以覆盖的 entry,则将新的 entry 插入在脏 entry


    tab[staleSlot].value = null;//8.


    tab[staleSlot] = new Entry(key, value);//9.


    // If there are any other stale entries in run, expunge them


    if (slotToExpunge != staleSlot)//10.


    //执行 cleanSomeSlots


    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);//11.


    }


    该方法的逻辑请看注释,下面我结合各种情况详细说一下该方法的执行过程。首先先看这一部分的代码:


    int slotToExpunge = staleSlot;


    for (int i = prevIndex(staleSlot, len);


    (e = tab[i]) != null;


    i = prevIndex(i, len))


    if (e.get() == null)


    slotToExpunge = i;


    这部分代码通过 PreIndex 方法实现往前环形搜索脏 entry 的功能,初始时 slotToExpunge 和 staleSlot 相同,若在搜索过程中发现了脏 entry,则更新 slotToExpunge 为当前索引 i。另外,说明 replaceStaleEntry 并不仅仅局限于处理当前已知的脏 entry,它认为在出现脏 entry 的相邻位置也有很大概率出现脏 entry,所以为了一次处理到位,就需要向前环形搜索,找到前面的脏 entry。那么根据在向前搜索中是否还有脏 entry 以及在 for 循环后向环形查找中是否找到可覆盖的 entry,我们分这四种情况来充分理解这个方法:


    • 1.前向有脏 entry


    1.1 后向环形查找找到可覆盖的 entry


    该情形如下图所示。



    如图,slotToExpunge 初始状态和 staleSlot 相同,当前向环形搜索遇到脏 entry 时,在第 1 行代码中 slotToExpunge 会更新为当前脏 entry 的索引 i,直到遇到哈希桶(table[i])为 null 的时候,前向搜索过程结束。在接下来的 for 循环中进行后向环形查找,若查找到了可覆盖的 entry,第 2,3,4 行代码先覆盖当前位置的 entry,然后再与 staleSlot 位置上的脏 entry 进行交换。交换之后脏 entry 就更换到了 i 处,最后使用 cleanSomeSlots 方法从 slotToExpunge 为起点开始进行清理脏 entry 的过程


    1.2 后向环形查找未找到可覆盖的 entry


    该情形如下图所示。



    如图,slotToExpunge 初始状态和 staleSlot 相同,当前向环形搜索遇到脏 entry 时,在第 1 行代码中 slotToExpunge 会更新为当前脏 entry 的索引 i,直到遇到哈希桶(table[i])为 null 的时候,前向搜索过程结束。在接下来的 for 循环中进行后向环形查找,若没有查找到了可覆盖的 entry,哈希桶(table[i])为 null 的时候,后向环形查找过程结束。那么接下来在 8,9 行代码中,将插入的新 entry 直接放在 staleSlot 处即可,最后使用 cleanSomeSlots 方法从 slotToExpunge 为起点开始进行清理脏 entry 的过程


    • 2.前向没有脏 entry


    2.1 后向环形查找找到可覆盖的 entry


    该情形如下图所示。



    如图,slotToExpunge 初始状态和 staleSlot 相同,当前向环形搜索直到遇到哈希桶(table[i])为 null 的时候,前向搜索过程结束,若在整个过程未遇到脏 entry,slotToExpunge 初始状态依旧和 staleSlot 相同。在接下来的 for 循环中进行后向环形查找,若遇到了脏 entry,在第 7 行代码中更新 slotToExpunge 为位置 i。若查找到了可覆盖的 entry,第 2,3,4 行代码先覆盖当前位置的 entry,然后再与 staleSlot 位置上的脏 entry 进行交换,交换之后脏 entry 就更换到了 i 处。如果在整个查找过程中都还没有遇到脏 entry 的话,会通过第 5 行代码,将 slotToExpunge 更新当前 i 处,最后使用 cleanSomeSlots 方法从 slotToExpunge 为起点开始进行清理脏 entry 的过程。


    2.2 后向环形查找未找到可覆盖的 entry


    该情形如下图所示。



    如图,slotToExpunge 初始状态和 staleSlot 相同,当前向环形搜索直到遇到哈希桶(table[i])为 null 的时候,前向搜索过程结束,若在整个过程未遇到脏 entry,slotToExpunge 初始状态依旧和 staleSlot 相同。在接下来的 for 循环中进行后向环形查找,若遇到了脏 entry,在第 7 行代码中更新 slotToExpunge 为位置 i。若没有查找到了可覆盖的 entry,哈希桶(table[i])为 null 的时候,后向环形查找过程结束。那么接下来在 8,9 行代码中,将插入的新 entry 直接放在 staleSlot 处即可。另外,如果发现 slotToExpunge 被重置,则第 10 行代码 if 判断为 true,就使用 cleanSomeSlots 方法从 slotToExpunge 为起点开始进行清理脏 entry 的过程。


    下面用一个实例来有个直观的感受,示例代码就不给出了,代码 debug 时 table 状态如下图所示:



    如图所示,当前的 staleSolt 为 i=4,首先先进行前向搜索脏 entry,当 i=3 的时候遇到脏 entry,slotToExpung 更新为 3,当 i=2 的时候 tabel[2]为 null,因此前向搜索脏 entry 的过程结束。然后进行后向环形查找,知道 i=7 的时候遇到 table[7]为 null,结束后向查找过程,并且在该过程并没有找到可以覆盖的 entry。最后只能在 staleSlot(4)处插入新 entry,然后从 slotToExpunge(3)为起点进行 cleanSomeSlots 进行脏 entry 的清理。是不是上面的 1.2 的情况。


    这些核心方法,通过源码又给出示例图,应该最终都能掌握了,也还挺有意思的。若觉得不错,对我的辛劳付出能给出鼓励欢迎点赞,给小弟鼓励,在此谢过 ??。


    当我们调用 threadLocal 的 get 方法时,当 table[i]不是和所要找的 key 相同的话,会继续通过 threadLocalMap 的


    getEntryAfterMiss 方法向后环形去找,该方法为:


    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {


    Entry[] tab = table;


    int len = tab.length;


    while (e != null) {


    ThreadLocal<?> k = e.get();


    if (k == key)


    return e;


    if (k == null)


    expungeStaleEntry(i);


    else


    i = nextIndex(i, len);


    e = tab[i];


    }


    return null;


    }


    当 key==null 的时候,即遇到脏 entry 也会调用 expungeStleEntry 对脏 entry 进行清理。


    当我们调用 threadLocal.remove 方法时候,实际上会调用 threadLocalMap 的 remove 方法,该方法的源码为:


    private void remove(ThreadLocal<?> key) {


    Entry[] tab = table;


    int len = tab.length;


    int i = key.threadLocalHashCode & (len-1);


    for (Entry e = tab[i];


    e != null;


    e = tab[i = nextIndex(i, len)]) {


    if (e.get() == key) {


    e.clear();


    expungeStaleEntry(i);


    return;


    }


    }


    }


    同样的可以看出,当遇到了 key 为 null 的脏 entry 的时候,也会调用 expungeStaleEntry 清理掉脏 entry。


    从以上 set,getEntry,remove 方法看出,在 threadLocal 的生命周期里,针对 threadLocal 存在的内存泄漏的问题,都会通过 expungeStaleEntry,cleanSomeSlots,replaceStaleEntry 这三个方法清理掉 key 为 null 的脏 entry

    为什么使用弱引用?

    从文章开头通过 threadLocal,threadLocalMap,entry 的引用关系看起来 threadLocal 存在内存泄漏的问题似乎是因为 threadLocal 是被弱引用修饰的。那为什么要使用弱引用呢?


    如果使用强引用


    假设 threadLocal 使用的是强引用,在业务代码中执行threadLocalInstance==null操作,以清理掉 threadLocal 实例的目的,但是因为 threadLocalMap 的 Entry 强引用 threadLocal,因此在 gc 的时候进行可达性分析,threadLocal 依然可达,对 threadLocal 并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误


    如果使用弱引用


    假设 Entry 弱引用 threadLocal,尽管会出现内存泄漏的问题,但是在 threadLocal 的生命周期里(set,getEntry,remove)里,都会针对 key 为 null 的脏 entry 进行处理。


    从以上的分析可以看出,使用弱引用的话在 threadLocal 生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。

    Thread.exit()

    当线程退出时会执行 exit 方法:


    private void exit() {


    if (group != null) {


    group.threadTerminated(this);


    group = null;


    }


    /* Aggressively null out all reference fields: see bug 4006245 */


    target = null;


    /* Speed the release of some of these resources */


    threadLocals = null;


    inheritableThreadLocals = null;


    inheritedAccessControlContext = null;


    blocker = null;


    uncaughtExceptionHandler = null;


    }


    从源码可以看出当线程结束时,会令 threadLocals=null,也就意味着 GC 的时候就可以将 threadLocalMap 进行垃圾回收,换句话说 threadLocalMap 生命周期实际上 thread 的生命周期相同。


    ThreadLocal 最佳实践

    评论

    发布
    暂无评论
    ThreadLocal内存泄漏分析与解决方案(1),linux文件系统原理