詳細解讀ThreadLocal的內存泄露
?說到內存溢出,我相信各位都知道是什么,但是說到內存泄露,而且還是 ThreadLocal ,阿粉就得來說一下這個了,畢竟如果面試的時候被問到 ThreadLocal 的內存泄露,是不是有可能不太了解了呢,今天阿粉來說一下這個 ThreadLocal 的內存泄露的原因,以及如何從開發(fā)中去避免這個問題。
什么是內存泄露
說到內存泄露,阿粉就得說一下,這個可能對于初中級的程序員來說,還是比較陌生的,為什么這么說,是因為,JVM 有自己的內存回收機制,所以對于初中級的程序員來說,很少有接觸到這個的,而內存泄露的意思呢,就是為程序在申請內存后,無法釋放已申請的內存空間,一次內存泄露危害可以忽略,但內存泄露堆積后果很嚴重,無論多少內存,遲早會被占光。
我們也都知道,我們有時候在定義變量的時候,就應該明白,他需要一段內存空間來存儲這個數據信息,而這段內存如果一直不被釋放,那么就會導致,內存被占用光,而被占用的這個對象,一直不能被回收掉,這就是內存泄漏。
在說 ThreadLocal 的內存泄露之前,我們先說說 ThreadLocal 的實現原理,然后我們再分析,他是泄露的原因是什么?
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
每一個 ThreadLocal 維護一個 ThreadLocalMap,key為使用 弱引用 的 ThreadLocal 實例,value為線程變量的副本。
那么什么又是弱引用呢?
引用的話,其實分為了兩類,既然有弱引用,那么必然的,就會有強引用,所以我們得區(qū)分開這個強引用和弱引用。
強引用
使用最普遍的引用,一個對象具有強引用,不會被垃圾回收器回收。當內存空間不足,Java虛 擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不回收這種對象。
如果想取消強引用和某個對象之間的關聯,可以顯式地將引用賦值為null,這樣可以使JVM在合適的時 間就會回收該對象。
弱引用
JVM進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在 java 中,用 java.lang.ref.WeakReference 類來表示。可以在緩存中使用弱引用。
上面阿粉也說了,每一個 ThreadLocal 維護一個 ThreadLocalMap,key為使用 弱引用 的 ThreadLocal 實例,value為線程變量的副本。而他們之間的引用關系如下圖:
在這個圖中
- 實線表示-強引用
- 虛線表示-弱引用
從圖中,我們就能分析出這個 ThreadLocal 是怎么出現內存泄露的了,我們從圖中能夠看到,ThreadLocalMap 使用 ThreadLocal 的弱引用作為key,如果一個ThreadLocal不存在外部強引用時,Key(ThreadLocal)勢必會被GC回收,這樣就會導致 ThreadLocalMap 中 key 為null, 而 value 還存在著強引用,只有 Thead 線程退出以后, value 的強引用鏈條才會斷掉。
但如果當前線程再遲遲不結束的話,這些key為null的Entry的value就會一直存在一條強引用鏈:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
這個時候,永遠無法回收,就會造成 ThreadLocal 出現內存泄露的問題了。
這個時候就有讀者會問,為什么 ThreaLocalMap 使用 ThreadLocal 的弱引用,而不去使用強引用,使用強引用的話,是不是就不會出現這個內存泄露的問題了呢?
其實這完全不是一回事,因為如果這時候 ThreaLocalMap 的 key 為強引用回收 ThreadLocal 時,因為 ThreadLocalMap 還持有 ThreadLocal 的強引用,如果沒有手動刪除,ThreadLocal 不會被回收,導致Entry內存泄漏。
這是使用強引用的時候,那么使用弱引用的時候是什么樣的呢?
當 ThreadLocalMap 的 key 為弱引用回收 ThreadLocal 時,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使沒有手動刪除,ThreadLocal 也會被回收。當key為null,在下一次ThreadLocalMap調用set(),get(),remove()方法的時候會被清除value值。
所以這么看下來,反而使用弱引用,卻是更好的為什么呢?
因為使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次 ThreadLocalMap調用set(),get(),remove()的時候會被清除。
因此,ThreadLocal內存泄漏的根本原因是:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。
那么我們繼續(xù)來看看這個 ThreadLocal 中維護的 ThreadLocalMap 的源碼分析
static class ThreadLocalMap {
//hreadLocalMap中數據是存儲在Entry類型數組的table中的,Entry繼承了WeakReference(弱引用)
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
**成員變量**
//初始容量
private static final int INITIAL_CAPACITY = 16;
//ThreadLocalMap數據真正存儲在table中
private Entry[] table;
//ThreadLocalMap條數
private int size = 0;
//達到這個大小,則擴容
private int threshold;
構造函數
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table數組,INITIAL_CAPACITY默認值為16
table = new Entry[INITIAL_CAPACITY];
//key和16取得哈希值
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//創(chuàng)建節(jié)點,設置key-value
table[i] = new Entry(firstKey, firstValue);
size = 1;
//設置擴容閾值
setThreshold(INITIAL_CAPACITY);
}
我們再來看看他的 remove 方法;
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//如果threadLocalHashCode計算出的下標找到的key和傳入key不同,則證明出現哈希沖突,則循環(huán)向下查找
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
//如果key相同
if (e.get() == key) {
//刪除當前Entry
e.clear();
//清理
expungeStaleEntry(i);
return;
}
}
}
ThreadLocal 中的 ThreadLocalMap 里面還有 set 和 getEntry 方法,還有很多,阿粉就不多介紹了。
那么我們應該如何合理的使用 ThreadLocal 才能保證內存不泄露呢?
- 每次使用完ThreadLocal都調用它的remove()方法清除數據
- 將ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強引用,也就能保證任何時候都能通過ThreadLocal的弱引用訪問到Entry的value值,進而清除掉 。