證明:ThreadLocal的get,set方法無法防止內存泄漏


  先給出結論:get,set兩個方法都不能完全防止內存泄漏,還是每次用完ThreadLocal都勤奮的remove一下靠譜。 

  前言: 

  看到有的博客說在把ThreadLocal的所有強引用置空前,調用 set 或 get 方法的話,則可以防止這個失去所有強引用的ThreadLocal對應的value內存泄漏。  但是文章作者一般沒有接着向下講為什么get,set 方法能防止內存泄漏。 

  本着刨根問底的精神,我們來看看原實現,驗證一下get,set方法是否真的能防止內存泄漏。

 

先介紹一下內存布局:

  每個Thread保存自己獨占的ThreadLocalMap,ThreadLocalMap包含一個散列表(entry數組),散列表里 entry 繼承WeakReference<ThreadLocal>,並且 entry 的 key 隱式等於ThreadLocal, value 則是顯示用成員變量來存儲。

所以一個線程可以用不同的ThreadLocal把不同的值存在這個線程獨享的散列表的不同位置上。下面這些entry的key就是不同的ThreadLocal。當有外部的強引用 使用ThreadLocal的時候,這個ThreadLocal是有效的,但是如果強引用都置空,則只剩弱引用,GC在內存緊張的情況下,可能會把弱引用指向的對象回收掉。

1.ThreadLocal還有效

 

 

 

2.ThreadLocal只剩下弱引用

3.只剩弱引用,回收堆上對象

 

 

 

 

 

這樣的話,就沒有路徑可以訪問這個ThreadLocal了。

但是value還是通過ThreadLocalMap -> entry -> value -> 堆上大對象 的方式強應用着之前的value。這樣導致這塊內存無法被使用(如果沒有其他強應用的話),也無法被回收。稱內存泄漏。

 

於是ThreadLocalMap的設計者,想出了辦法:

1.在ThreadLocal get,set 的時候順帶把散列表中的無效entry 置空,並且把這些entry 的 value也置空,以便value被回收,也就是執行清掃操作

2.在ThreadLocal remove 的時候把對應槽位上的 entry 置空,並且把這 個entry 的 value也置空,以便value被回收。順便執行清掃操作。

 

get,set 方法真的能保證內存不泄露么?

這篇文章想討論的問題是:

1.get,set方法的清掃程度是否足夠徹底,以至於可以防止內存泄漏。

2.用什么方法才能保證內存不泄露

 

1如果成立,也即是保證如下場景內存不泄露:

使用多個 ThreadLocal,不是每次都使用 remove 方法,並且把一個ThreadLocal對應的所有強應用置空之前只調用過 get, set方法,調用get,set方法可以防止內存泄漏。

為了打破這一假設,模擬內存泄漏的情況,舉以下極端的例子:

 

先規定:

 

1.一開始都是有效的entry,並且每個entry的key通過散列算法后算出的位置都是自己所在的位置(都在自己的位置上的話之后的線性清掃中不會造成搬移,因為ThreadLocalMap的散列表用的是開放定址法,所以entry可能因為hash沖突而不在自己位置上)

要達成下面的效果,就要一直沒有失效的entry出現,並且一直實現插入,也就是一直執行set方法

假設entry數組有32個槽位

 

 

如果執行一次remove,把圖中的某個entry無效化。

   

 

 下面是實現,因為每個entry都在自己的位置上,所以下圖的if (e.get() == key) 會在第一個循環就成立,也就是remove會

 執行e.clear() 來把弱引用置空,無效化。並且執行一次線性清掃后返回。

 

 

關於線性清掃:

  實現較長,分段看:

 

 

  上來就把要清掃的位置給置空了(灰色entry的槽位置空):

 

接着看:

 

 

 向后遍歷整個數組,直到遇到空槽為止,並且第一種情況 (k == null) 為真的情況下,會把無效entry置空,防止內存泄漏。

 其實就是向后掃描,遇到無效的就順帶干掉,直到遇到空位置為止。

 第二種情況是 : 遇到的entry是有效的,但是不是在自己原本的位置上,而是被hash沖突所迫而在其他位置上的,則把他們搬去

 離原本位置最近的后邊空槽上。這樣在get的時候,會最快找到這個entry,減少開放定址法遍歷數組的時間。

 

 因為每個entry都在自己的位置上,並且沒有遇到無效的entry,最終的效果只是把remove的位置置為空槽。

同理,經過幾次remove后,我們可以“挖出”下圖的效果

 

 

 

正巧,這時候有兩個entry的key,也即是ThreadLocal的所有強應用被置空,於是這兩個entry無效。

     

 

 

 

 

如果之后只執行 set 方法,是否會內存泄漏呢?是否任意調用set之后就保證內存不會泄漏了呢?

我們順着 set 方法的邏輯看下去,set方法從當前要set的位置開始向后遍歷,直到:

1.遇到 key 和我們當前 調用 set 的 ThreadLocal 相等的 entry,則只用直接把entry的value設置以下就好了,和

HashMap的 put(1, A); put(1, B); 中 A 被替換 成B 同理。(紅色框)

2.遇到無效entry,是我們關注的地方

3.遇到空槽,直接插入,並且嘗試指數清掃,如果指數清掃不成功並且當前entry的使用槽數到達閾值則重散列(藍色框)

 

 

 

 

我們重點關注情況2.

假設我們set的位置是下面所指處。

我們接着上面的2分析,2要調用到replaceStaleEntry

再假設set進去的ThreadLocal在本數組中下面綠色位置

綠色代表這個entry不在自己的原本位置上,上面的情況是可以得到的。因為假如綠色本來散列到的位置上有元素,那么將會向后線性掃描,發現空位,插入綠色元素,然后向后線性清掃是向后清掃,並且遇到空槽停下。

所以不會影響綠色entry

 

 

 

 

 

 方法一開始是找到最靠前的無效entry,直到遇到空槽為止,當然可能會繞數組一圈繞回來

但是因為使用的槽數如果到達閾值,就會rehash,不可能所有槽都用完。所以會遇到空槽的。

表現在我們的例子中:

 

 

因為沒有找到,所以 slotToExpunge = staleSlot

也就是上圖第二個灰色entry的位置。

 接着向下看:

 

 

 我們關注 k == key 的情況,也就是 i 遍歷圖中綠色槽位的情況。 這種情況下會指向一次線性清掃,然后執行對數清掃。之后返回。

 

 

反應在圖例中:

 

 

從slotToExpunge位置開始,先進行一輪線性清掃:

同之前一樣,一上來先把待清掃槽位置空(第二個灰色的entry的位置),之后遇到第二個灰色后面那個空位,所以停下來。

線性清掃返回空位的下標做為參數傳給對數清掃。

反應到圖例:

對數清掃:清掃次數 = log2(N) ,N是散列表大小,本例中是32,所以要清掃5次,每次清掃是通過調用線性清掃實現的。

並且只有遇到無效entry時才執行線性清掃。

 

 顯然,五次掃描中都沒有無效entry

 

返回 removed (false);

 cleanSomeSlots要返回,一直返回到replaceStaleEntry,並且繼續返回,最后從set方法返回。

 

 

 結果很明顯,第一個灰色entry未被清除。

結論:set方法的清掃程度不夠深,set方法並不能防止內存泄漏。

get方法呢?

 

 

 

 

 

 get 方法比較簡單,在原本屬於當前 key 的位置上找不到當前 key 的 entry 的話,就會根據開放定址法線性遍歷找到 key 對應的 entry 。

順便把路上無效的entry用線性清掃清除掉。

還是剛剛的極端例子:

 

 

 

 

 

 因為是直接取線性清掃開始的位置,所以 k = key 是 true,所以返回綠色entry。查找成功

 

 

 但是,第一個灰色entry仍然沒有被清除。

 什么辦法可以保證萬無一失呢???

答:每次置空一個ThreadLocal的所有強引用之后,都調用ThreadLocal的remove方法:

 

e.clear是直接置空弱引用,這樣當前這個entry就會無效

 

 

之前說過,線性清掃會直接把第一個無效entry,也就是起點的entry槽位置空,以此達到 100 % 的回收效果。

 

結論:

get,set兩個方法都不能完全防止內存泄漏,還是每次用完ThreadLocal都勤奮的remove一下靠譜。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

 

 

 

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM