阿粉昨天說(shuō)我動(dòng)不動(dòng)就內(nèi)存泄漏,我好委屈...
大家好,我是 ThreadLocal ,昨天阿粉說(shuō)我動(dòng)不動(dòng)就內(nèi)存泄漏,我蠻委屈的,我才沒(méi)有冤枉他嘞,證據(jù)在這里: ThreadLocal 你怎么動(dòng)不動(dòng)就內(nèi)存泄漏?
因?yàn)槿思颐髅饕部紤]到了很多情況,做了很多事情,保證了如果沒(méi)有 remove ,也有對(duì) key 值為 null 時(shí)進(jìn)行回收的處理操作
啥?你竟然不信?我 ThreadLocal 難道會(huì)騙你么
今天為了證明一下自己,我打算從組成的源碼開(kāi)始講起,在 get , set 方法中都有對(duì) key 值為 null 時(shí)進(jìn)行回收的處理操作,先來(lái)看 set 方法是怎么做的
set
下面是 set 方法的源碼:
- private void set(ThreadLocal<?> key, Object value) {
- // We don't use a fast path as with get() because it is at
- // least as common to use set() to create new entries as
- // it is to replace existing ones, in which case, a fast
- // path would fail more often than not.
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- // 如果 e 不為空,說(shuō)明 hash 沖突,需要向后查找
- e != null;
- // 從這里可以看出, ThreadLocalMap 采用的是開(kāi)放地址法解決的 hash 沖突
- // 是最經(jīng)典的 線(xiàn)性探測(cè)法 --> 我覺(jué)得之所以選擇這種方法解決沖突時(shí)因?yàn)閿?shù)據(jù)量不大
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal<?> k = e.get();
- // 要查找的 ThreadLocal 對(duì)象找到了,直接設(shè)置需要設(shè)置的值,然后 return
- if (k == key) {
- e.value = value;
- return;
- }
- // 如果 k 為 null ,說(shuō)明有 value 沒(méi)有及時(shí)回收,此時(shí)通過(guò) replaceStaleEntry 進(jìn)行處理
- // replaceStaleEntry 具體內(nèi)容等下分析
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- // 如果 tab[i] == null ,則直接創(chuàng)建新的 entry 即可
- tab[i] = new Entry(key, value);
- int sz = ++size;
- // 在創(chuàng)建之后調(diào)用 cleanSomeSlots 方法檢查是否有 value 值沒(méi)有及時(shí)回收
- // 如果 sz >= threshold ,則需要擴(kuò)容,重新 hash 即, rehash();
- if (!cleanSomeSlots(i, sz) && sz >= threshold)
- rehash();
- }
通過(guò)源碼可以看到,在 set 方法中,主要是通過(guò) replaceStaleEntry 方法和 cleanSomeSlots 方法去做的檢測(cè)和處理
接下來(lái)瞅瞅 replaceStaleEntry 都干了點(diǎn)兒啥
replaceStaleEntry
- private void replaceStaleEntry(ThreadLocal<?> key, Object value,
- int staleSlot) {
- Entry[] tab = table;
- int len = tab.length;
- Entry e;
- // 從當(dāng)前 staleSlot 位置開(kāi)始向前遍歷
- int slotToExpunge = staleSlot;
- for (int i = prevIndex(staleSlot, len);
- (e = tab[i]) != null;
- i = prevIndex(i, len))
- if (e.get() == null)
- // 當(dāng) e.get() == null 時(shí), slotToExpunge 記錄下此時(shí)的 i 值
- // 即 slotToExpunge 記錄的是 staleSlot 左手邊第一個(gè)空的 Entry
- slotToExpunge = i;
- // 接下來(lái)從當(dāng)前 staleSlot 位置向后遍歷
- // 這兩個(gè)遍歷是為了清理在左邊遇到的第一個(gè)空的 entry 到右邊的第一個(gè)空的 entry 之間所有過(guò)期的對(duì)象
- // 但是如果在向后遍歷過(guò)程中,找到了需要設(shè)置值的 key ,就開(kāi)始清理,不會(huì)再繼續(xù)向下遍歷
- for (int i = nextIndex(staleSlot, len);
- (e = tab[i]) != null;
- i = nextIndex(i, len)) {
- ThreadLocal<?> k = e.get();
- // 如果 k == key 說(shuō)明在插入之前就已經(jīng)有相同的 key 值存在,所以需要替換舊的值
- // 同時(shí)和前面過(guò)期的對(duì)象進(jìn)行交換位置
- if (k == key) {
- e.value = value;
- tab[i] = tab[staleSlot];
- tab[staleSlot] = e;
- // 如果 slotToExpunge == staleSlot 說(shuō)明向前遍歷時(shí)沒(méi)有找到過(guò)期的
- if (slotToExpunge == staleSlot)
- slotToExpunge = i;
- // 進(jìn)行清理過(guò)期數(shù)據(jù)
- cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
- return;
- }
- // 如果在向后遍歷時(shí),沒(méi)有找到 value 被回收的 Entry 對(duì)象
- // 且剛開(kāi)始 staleSlot 的 key 為空,那么它本身就是需要設(shè)置 value 的 Entry 對(duì)象
- // 此時(shí)不涉及到清理
- if (k == null && slotToExpunge == staleSlot)
- slotToExpunge = i;
- }
- // 如果 key 在數(shù)組中找不到,那就好說(shuō)了,直接創(chuàng)建一個(gè)新的就可以了
- tab[staleSlot].value = null;
- tab[staleSlot] = new Entry(key, value);
- // 如果 slotToExpunge != staleSlot 說(shuō)明存在過(guò)期的對(duì)象,就需要進(jìn)行清理
- if (slotToExpunge != staleSlot)
- cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
- }
在 replaceStaleEntry 方法中,需要注意一下剛開(kāi)始的兩個(gè) for 循環(huán)中內(nèi)容(在這里再貼一下):
- if (e.get() == null)
- // 當(dāng) e.get() == null 時(shí), slotToExpunge 記錄下此時(shí)的 i 值
- // 即 slotToExpunge 記錄的是 staleSlot 左手邊第一個(gè)空的 Entry
- slotToExpunge = i;
- if (k == key) {
- e.value = value;
- tab[i] = tab[staleSlot];
- tab[staleSlot] = e;
- // 如果 slotToExpunge == staleSlot 說(shuō)明向前遍歷時(shí)沒(méi)有找到過(guò)期的
- if (slotToExpunge == staleSlot)
- slotToExpunge = i;
- // 進(jìn)行清理過(guò)期數(shù)據(jù)
- cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
- return;
- }
這兩個(gè) for 循環(huán)中的 if 到底是在做什么?
看第一個(gè) if ,當(dāng) e.get() == null 時(shí),此時(shí)將 i 的值給 slotToExpunge
第二個(gè) if ,當(dāng) k ==key 時(shí),此時(shí)將 i 給了 staleSlot 來(lái)進(jìn)行交換
為什么要對(duì) staleSlot 進(jìn)行交換呢?畫(huà)圖說(shuō)明一下
如下圖,假設(shè)此時(shí)表長(zhǎng)為 10 ,其中下標(biāo)為 3 和 5 的 key 已經(jīng)被回收( key 被回收掉的就是 null ),因?yàn)椴捎玫拈_(kāi)放地址法,所以 15 mod 10 應(yīng)該是 5 ,但是因?yàn)槲恢帽徽迹栽?6 的位置,同樣 25 mod 10 也應(yīng)該是 5 ,但是因?yàn)槲恢帽徽迹聜€(gè)位置也被占,所以就在第 7 號(hào)的位置上了
按照上面的分析,此時(shí) slotToExpunge 值為 3 , staleSlot 值為 5 , i 為 6
假設(shè),假設(shè)這個(gè)時(shí)候如果不進(jìn)行交換,而是直接回收的話(huà),此時(shí)位置為 5 的數(shù)據(jù)就被回收掉,然后接下來(lái)要插入一個(gè) key 為 15 的數(shù)據(jù),此時(shí) 15 mod 10 算出來(lái)是 5 ,正好這個(gè)時(shí)候位置為 5 的被回收完畢,這個(gè)位置就被空出來(lái)了,那么此時(shí)就會(huì)這樣:
同樣的 key 值竟然出現(xiàn)了兩次?!
這肯定是不希望看到的結(jié)果,所以一定要進(jìn)行數(shù)據(jù)交換
在上面代碼中有一行代碼 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); ,說(shuō)明接下來(lái)的處理是交給了 expungeStaleEntry ,接下來(lái)去分析一下 expungeStaleEntry
expungeStaleEntry
- 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();
- // 如果 k == null ,說(shuō)明 value 就應(yīng)該被回收掉
- if (k == null) {
- // 此時(shí)直接將 e.value 置為 null
- // 這樣就將 thread -> threadLocalMap -> value 這條引用鏈給打破
- // 方便了 GC
- e.value = null;
- tab[i] = null;
- size--;
- } else {
- // 這個(gè)時(shí)候要重新 hash ,因?yàn)椴捎玫氖情_(kāi)放地址法,所以可以理解為就是將后面的元素向前移動(dòng)
- 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;
- }
因?yàn)槭窃?replaceStaleEntry 方法中調(diào)用的此方法,傳進(jìn)來(lái)的值是 staleSlot ,繼續(xù)上圖,經(jīng)過(guò) replaceStaleEntry 之后,它的數(shù)據(jù)結(jié)構(gòu)是這樣:
此時(shí)傳進(jìn)來(lái)的 staleSlot 值為 6 ,因?yàn)榇藭r(shí)的 key 為 null ,所以接下來(lái)會(huì)走 e.value = null ,這一步結(jié)束之后,就成了:
接下來(lái) i 為 7 ,此時(shí)的 key 不為 null ,那么就會(huì)重新 hash : int h = k.threadLocalHashCode & (len - 1); ,得到的 h 應(yīng)該是 5 ,但是實(shí)際上 i 為 7 ,說(shuō)明出現(xiàn)了 hash 沖突,就會(huì)繼續(xù)向下走,最終的結(jié)果是這樣:
可以看到,原來(lái)的 key 為 null ,值為 V5 的已經(jīng)被回收掉了。我認(rèn)為之所以回收掉之后,還要再次進(jìn)行重新 hash ,就是為了防止 key 值重復(fù)插入情況的發(fā)生
假設(shè) key 為 25 的并沒(méi)有進(jìn)行向前移動(dòng),也就是它還在位置 7 ,位置 6 是空的,再插入一個(gè) key 為 25 ,經(jīng)過(guò) hash 應(yīng)該在位置 5 ,但是有數(shù)據(jù)了,那就向下走,到了位置 6 ,誒,竟然是空的,趕緊插進(jìn)去,這不就又造成了上面說(shuō)到的問(wèn)題,同樣的一個(gè) key 竟然出現(xiàn)了兩次?!
而且經(jīng)過(guò) expungeStaleEntry 之后,將 key 為 null 的值,也設(shè)置為了 null ,這樣就方便 GC
分析到這里應(yīng)該就比較明確了,在 expungeStaleEntry 中,有些地方是幫助 GC 的,而通過(guò)源碼能夠發(fā)現(xiàn), set 方法調(diào)用了該方法進(jìn)行了 GC 處理, get 方法也有,不信你瞅瞅:
get
- private Entry getEntry(ThreadLocal<?> key) {
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- // 如果能夠找到尋找的值,直接 return 即可
- if (e != null && e.get() == key)
- return e;
- else
- // 如果找不到,則調(diào)用 getEntryAfterMiss 方法去處理
- return getEntryAfterMiss(key, i, e);
- }
- private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
- Entry[] tab = table;
- int len = tab.length;
- // 一直探測(cè)尋找下一個(gè)元素,直到找到的元素是要找的
- while (e != null) {
- ThreadLocal<?> k = e.get();
- if (k == key)
- return e;
- if (k == null)
- // 如果 k == null 說(shuō)明有 value 沒(méi)有及時(shí)回收
- // 調(diào)用 expungeStaleEntry 方法去處理,幫助 GC
- expungeStaleEntry(i);
- else
- i = nextIndex(i, len);
- e = tab[i];
- }
- return null;
- }
get 和 set 方法都有進(jìn)行幫助 GC ,所以正常情況下是不會(huì)有內(nèi)存溢出的,但是如果創(chuàng)建了之后一直沒(méi)有調(diào)用 get 或者 set 方法,還是有可能會(huì)內(nèi)存溢出
所以最保險(xiǎn)的方法就是,使用完之后就及時(shí) remove 一下,加快垃圾回收,就完美的避免了垃圾回收
我 ThreadLocal 雖然沒(méi)辦法做到 100% 的解決內(nèi)存泄漏問(wèn)題,但是我能做到 80% 不也應(yīng)該夸夸我嘛