前言
在分析ThreadLocal導致的記憶體洩露前,需要普及瞭解一下記憶體洩露、強引用與弱引用以及GC回收機制,這樣才能更好的分析為什麼ThreadLocal會導致記憶體洩露呢?更重要的是知道該如何避免這樣情況發生,增強系統的健壯性。
記憶體洩露
記憶體洩露為程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩露危害可以忽略,但記憶體洩露堆積後果很嚴重,無論多少記憶體,遲早會被佔光,
廣義並通俗的說,就是:不再會被使用的物件或者變數佔用的記憶體不能被回收,就是記憶體洩露。
強引用與弱引用
強引用,使用最普遍的引用,一個物件具有強引用,不會被垃圾回收器回收。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不回收這種物件。
如果想取消強引用和某個物件之間的關聯,可以顯式地將引用賦值為null,這樣可以使JVM在合適的時間就會回收該物件。
弱引用,JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的物件。在java中,用java.lang.ref.WeakReference類來表示。可以在快取中使用弱引用。
GC回收機制-如何找到需要回收的物件
JVM如何找到需要回收的物件,方式有兩種:
-
引用計數法:每個物件有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收,
-
可達性分析法:從 GC Roots 開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是不可用的,那麼虛擬機器就判斷是可回收物件。
引用計數法,可能會出現A 引用了 B,B 又引用了 A,這時候就算他們都不再使用了,但因為相互引用 計數器=1 永遠無法被回收。
ThreadLocal的記憶體洩露分析
先從前言的瞭解了一些概念(已懂忽略),接下來我們開始正式的來理解ThreadLocal導致的記憶體洩露的解析。
實現原理
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... }
ThreadLocal的實現原理,每一個Thread維護一個ThreadLocalMap,key為使用弱引用的ThreadLocal例項,value為執行緒變數的副本。這些物件之間的引用關係如下,
實心箭頭表示強引用,空心箭頭表示弱引用
ThreadLocal 記憶體洩漏的原因
從上圖中可以看出,hreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal不存在外部強引用時,Key(ThreadLocal)勢必會被GC回收,這樣就會導致ThreadLocalMap中key為null, 而value還存在著強引用,只有thead執行緒退出以後,value的強引用鏈條才會斷掉。
但如果當前執行緒再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永遠無法回收,造成記憶體洩漏。
那為什麼使用弱引用而不是強引用??
我們看看Key使用的
key 使用強引用
當hreadLocalMap的key為強引用回收ThreadLocal時,因為ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry記憶體洩漏。
key 使用弱引用
當ThreadLocalMap的key為弱引用回收ThreadLocal時,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。當key為null,在下一次ThreadLocalMap呼叫set(),get(),remove()方法的時候會被清除value值。
ThreadLocalMap的remove()分析
在這裡只分析remove()方式,其他的方法可以檢視原始碼進行分析:
private void remove(ThreadLocal<?> key) { //使用hash方式,計算當前ThreadLocal變數所在table陣列位置 Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //再次迴圈判斷是否在為ThreadLocal變數所在table陣列位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { //呼叫WeakReference的clear方法清除對ThreadLocal的弱引用 e.clear(); //清理key為null的元素 expungeStaleEntry(i); return; } } }
再看看清理key為null的元素expungeStaleEntry(i):
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 根據強引用的取消強引用關聯規則,將value顯式地設定成null,去除引用 tab[staleSlot].value = null; tab[staleSlot] = null; size--; // 重新hash,並對table中key為null進行處理 Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //對table中key為null進行處理,將value設定為null,清除value的引用 if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
總結
由於Thread中包含變數ThreadLocalMap,因此ThreadLocalMap與Thread的生命週期是一樣長,如果都沒有手動刪除對應key,都會導致記憶體洩漏。
但是使用弱引用可以多一層保障:弱引用ThreadLocal不會記憶體洩漏,對應的value在下一次ThreadLocalMap呼叫set(),get(),remove()的時候會被清除。
因此,ThreadLocal記憶體洩漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長,如果沒有手動刪除對應key就會導致記憶體洩漏,而不是因為弱引用。
ThreadLocal正確的使用方法
-
每次使用完ThreadLocal都呼叫它的remove()方法清除資料
-
將ThreadLocal變數定義成private static,這樣就一直存在ThreadLocal的強引用,也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值,進而清除掉 。
各位看官還可以嗎?喜歡的話,動動手指點個?,點個關注唄!!謝謝支援!
歡迎關注公眾號【Ccww技術部落格】,原創技術文章第一時間推出