「漲薪祕訣」ThreadLocal的內存洩露的原因分析+避免方法

前言

在分析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導致的內存洩露的解析。

實現原理

<code>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>/<code>

ThreadLocal的實現原理,每一個Thread維護一個ThreadLocalMap,key為使用 弱引用 的ThreadLocal實例,value為線程變量的副本。這些對象之間的引用關係如下,

「漲薪秘訣」ThreadLocal的內存洩露的原因分析+避免方法

實心箭頭表示強引用,空心箭頭表示弱引用

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()方式,其他的方法可以查看源碼進行分析:

<code>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;
}
}
}/<code>

再看看清理key為null的元素expungeStaleEntry(i):

<code>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;
}/<code>

總結

由於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值,進而清除掉 。

最後

覺得此文不錯的大佬們可以多多關注或者幫忙轉發分享一下哦,感謝!!!!

「漲薪秘訣」ThreadLocal的內存洩露的原因分析+避免方法


分享到:


相關文章: