0
点赞
收藏
分享

微信扫一扫

ThreadLocal内存泄漏分析与解决方案

木匠0819 2022-04-24 阅读 87

/* @param i a position known NOT to hold a stale entry. The

  • scan starts at the element after i.

  • @param n scan control: {@code log2(n)} cells are scanned,

  • unless a stale entry is found, in which case

  • {@code log2(table.length)-1} additional cells are scanned.

  • When called from insertions, this parameter is the number

  • of elements, but when from replaceStaleEntry, it is the

  • table length. (Note: all this could be changed to be either

  • more or less aggressive by weighting n instead of just

  • using straight log n. But this version is simple, fast, and

  • seems to work well.)

  • @return true if any stale entries have been removed.

*/

private boolean cleanSomeSlots(int i, int n) 《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》无偿开源 威信搜索公众号【编程进阶路】 {

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;

}

入参:

  1. i表示:插入entry的位置i,很显然在上述情况2(table[i]==null)中,entry刚插入后该位置i很显然不是脏entry;

  2. 参数n

2.1. n的用途

主要用于扫描控制(scan control),从while中是通过n来进行条件判断的说明n就是用来控制扫描趟数(循环次数)的。在扫描过程中,如果没有遇到脏entry就整个扫描过程持续log2(n)次,log2(n)的得来是因为n >>>= 1,每次n右移一位相当于n除以2。如果在扫描过程中遇到脏entry的话就会令n为当前hash表的长度(n=len),再扫描log2(n)趟,注意此时n增加无非就是多增加了循环次数从而通过nextIndex往后搜索的范围扩大,示意图如下

cleanSomeSlots示意图.png

按照n的初始值,搜索范围为黑线,当遇到了脏entry,此时n变成了哈希数组的长度(n取值增大),搜索范围log2(n)增大,红线表示。如果在整个搜索过程没遇到脏entry的话,搜索结束,采用这种方式的主要是用于时间效率上的平衡。

2.2. n的取值

如果是在set方法插入新的entry后调用(上述情况2),n位当前已经插入的entry个数size;如果是在replaceSateleEntry方法中调用n为哈希表的长度len。

[](()expungeStaleEntry方法

如果对输入参数能够理解的话,那么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方法做一下总结,其方法执行示意图如下:

cleanSomeSlots示意图.png

如图所示,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 encountered while

  •     searching for key.
    

*/

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

该情形如下图所示。

举报

相关推荐

0 条评论