ThreadLocal記憶體洩漏真因探究
ThreadLocal原理回顧
ThreadLocal的原理:每個Thread內部維護著一個ThreadLocalMap,它是一個Map。這個對映表的Key是一個弱引用,其實就是ThreadLocal本身,Value是真正存的執行緒變數Object。
也就是說ThreadLocal本身並不真正儲存執行緒的變數值,它只是一個工具,用來維護Thread內部的Map,幫助存和取。注意上圖的虛線,它代表一個弱引用型別,而弱引用的生命週期只能存活到下次GC前。
ThreadLocal為什麼會記憶體洩漏
ThreadLocal在ThreadLocalMap中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那麼ThreadLocal會在下次JVM垃圾收集時被回收。這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前執行緒的生命週期很長,一直存在,那麼其內部的ThreadLocalMap物件也一直生存下來,這些null key就存在一條強引用鏈的關係一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成記憶體洩漏。
但是JVM團隊已經考慮到這樣的情況,並做了一些措施來保證ThreadLocal儘量不會記憶體洩漏:在ThreadLocal的get()、set()、remove()方法呼叫的時候會清除掉執行緒ThreadLocalMap中所有Entry中Key為null的Value,並將整個Entry設定為null,利於下次記憶體回收。
來看看ThreadLocal的get()方法底層實現
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); }
在呼叫map.getEntry(this)時,內部會判斷key是否為null,繼續看map.getEntry(this)原始碼
private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
在getEntry方法中,如果Entry中的key發現是null,會繼續呼叫getEntryAfterMiss(key, i, e)方法,其內部回做回收必要的設定,繼續看內部原始碼:
private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
注意k == null這裡,繼續呼叫了expungeStaleEntry(i)方法,expunge的意思是擦除,刪除的意思,見名知意,在來看expungeStaleEntry方法的內部實現:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot(意思是,刪除value,設定為null便於下次回收)
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();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
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;
}
注意這裡,將當前Entry刪除後,會繼續迴圈往下檢查是否有key為null的節點,如果有則一併刪除,防止記憶體洩漏。
但這樣也並不能保證ThreadLocal不會發生記憶體洩漏,例如:
- 使用static的ThreadLocal,延長了ThreadLocal的生命週期,可能導致的記憶體洩漏。
- 分配使用了ThreadLocal又不再呼叫get()、set()、remove()方法,那麼就會導致記憶體洩漏。
為什麼使用弱引用?
從表面上看,發生記憶體洩漏,是因為Key使用了弱引用型別。但其實是因為整個Entry的key為null後,沒有主動清除value導致。很多文章大多分析ThreadLocal使用了弱引用會導致記憶體洩漏,但為什麼使用弱引用而不是強引用?
官方文件的說法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了處理非常大和生命週期非常長的執行緒,雜湊表使用弱引用作為 key。
下面我們分兩種情況討論:
- key 使用強引用:引用的ThreadLocal的物件被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry記憶體洩漏。
- key 使用弱引用:引用的ThreadLocal的物件被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap呼叫set,get,remove的時候會被清除。
比較兩種情況,我們可以發現:由於ThreadLocalMap的生命週期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致記憶體洩漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會記憶體洩漏,對應的value在下一次ThreadLocalMap呼叫set,get,remove的時候會被清除。
因此,ThreadLocal記憶體洩漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長,如果沒有手動刪除對應key的value就會導致記憶體洩漏,而不是因為弱引用。
總結
綜合上面的分析,我們可以理解ThreadLocal記憶體洩漏的前因後果,那麼怎麼避免記憶體洩漏呢?
- 每次使用完ThreadLocal,都呼叫它的remove()方法,清除資料。
在使用執行緒池的情況下,沒有及時清理ThreadLocal,不僅是記憶體洩漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。
作者:Misout
連結:https://www.jianshu.com/p/a1cd61fa22da
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。