淺談ThreadLocal為什么會(huì)內(nèi)存泄漏
前言
如果線程使用線程池或者Thread長時(shí)間不會(huì)消亡,其內(nèi)部的threadLocalMap也一直存在。而thread.threadLocalMap.set(threadLocal,value)。
這里threadLocal為弱引用,(ThreadLocal#ThreadLocalMap#new Entry(threadLocal)產(chǎn)生的弱引用weakRef),value為強(qiáng)引用。
Entry中弱引用key對(duì)應(yīng)的threadLocal 會(huì)在gc的時(shí)候 回收,因此value對(duì)應(yīng)的key會(huì)變成null.value對(duì)應(yīng)的內(nèi)存就無法再被訪問,已經(jīng)泄露了。
不過好在threadLocal中 expungeStaleEntry(threadLocal調(diào)用get/set/remove觸發(fā)) 會(huì)清除key為null的value,一定程度解決了內(nèi)存泄漏的問題。
ps:當(dāng)threadLocal 不為靜態(tài)變量,且被回收的時(shí)候才會(huì)導(dǎo)致weakRef為null。
ThreadLocal原理回顧

ThreadLocal的原理:每個(gè)Thread內(nèi)部維護(hù)著一個(gè)ThreadLocalMap,它是一個(gè)Map。這個(gè)映射表的Key是一個(gè)弱引用,其實(shí)就是ThreadLocal本身,Value是真正存的線程變量Object。
也就是說ThreadLocal本身并不真正存儲(chǔ)線程的變量值,它只是一個(gè)工具,用來維護(hù)Thread內(nèi)部的Map,幫助存和取。注意上圖的虛線,它代表一個(gè)弱引用類型,而弱引用的生命周期只能存活到下次GC前。
ThreadLocal為什么會(huì)內(nèi)存泄漏
ThreadLocal在ThreadLocalMap中是以一個(gè)弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強(qiáng)引用來引用它,那么ThreadLocal會(huì)在下次JVM垃圾收集時(shí)被回收。
這個(gè)時(shí)候就會(huì)出現(xiàn)Entry中Key已經(jīng)被回收,出現(xiàn)一個(gè)null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。
因此如果當(dāng)前線程的生命周期很長,一直存在,那么其內(nèi)部的ThreadLocalMap對(duì)象也一直生存下來,這些null key就存在一條強(qiáng)引用鏈的關(guān)系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強(qiáng)引用鏈會(huì)導(dǎo)致Entry不會(huì)回收,Value也不會(huì)回收,但Entry中的Key卻已經(jīng)被回收的情況,造成內(nèi)存泄漏。
但是JVM團(tuán)隊(duì)已經(jīng)考慮到這樣的情況,并做了一些措施來保證ThreadLocal盡量不會(huì)內(nèi)存泄漏:在ThreadLocal的get()、set()、remove()方法調(diào)用的時(shí)候會(huì)清除掉線程ThreadLocalMap中所有Entry中Key為null的Value,并將整個(gè)Entry設(shè)置為null,利于下次內(nèi)存回收。
來看看ThreadLocal的get()方法底層實(shí)現(xiàn)
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();
}在調(diào)用map.getEntry(this)時(shí),內(nèi)部會(huì)判斷key是否為null,繼續(xù)看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發(fā)現(xiàn)是null,會(huì)繼續(xù)調(diào)用getEntryAfterMiss(key, i, e)方法,其內(nèi)部回做回收必要的設(shè)置,繼續(xù)看內(nèi)部源碼:
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這里,繼續(xù)調(diào)用了expungeStaleEntry(i)方法,expunge的意思是擦除,刪除的意思,見名知意,在來看expungeStaleEntry方法的內(nèi)部實(shí)現(xiàn):
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot(意思是,刪除value,設(shè)置為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;
}注意這里,將當(dāng)前Entry刪除后,會(huì)繼續(xù)循環(huán)往下檢查是否有key為null的節(jié)點(diǎn),如果有則一并刪除,防止內(nèi)存泄漏。
但這樣也并不能保證ThreadLocal不會(huì)發(fā)生內(nèi)存泄漏,例如:
使用static的ThreadLocal,延長了ThreadLocal的生命周期,可能導(dǎo)致的內(nèi)存泄漏。分配使用了ThreadLocal又不再調(diào)用get()、set()、remove()方法,那么就會(huì)導(dǎo)致內(nèi)存泄漏。
為什么使用弱引用?
從表面上看,發(fā)生內(nèi)存泄漏,是因?yàn)镵ey使用了弱引用類型。但其實(shí)是因?yàn)檎麄€(gè)Entry的key為null后,沒有主動(dòng)清除value導(dǎo)致。
很多文章大多分析ThreadLocal使用了弱引用會(huì)導(dǎo)致內(nèi)存泄漏,但為什么使用弱引用而不是強(qiáng)引用?
官方文檔的說法:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. 為了處理非常大和生命周期非常長的線程,哈希表使用弱引用作為 key。
下面我們分兩種情況討論:
key 使用強(qiáng)引用:引用的ThreadLocal的對(duì)象被回收了,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用,如果沒有手動(dòng)刪除,ThreadLocal不會(huì)被回收,導(dǎo)致Entry內(nèi)存泄漏。
key 使用弱引用:引用的ThreadLocal的對(duì)象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動(dòng)刪除,ThreadLocal也會(huì)被回收。
value在下一次ThreadLocalMap調(diào)用set,get,remove的時(shí)候會(huì)被清除。
比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動(dòng)刪除對(duì)應(yīng)key,都會(huì)導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會(huì)內(nèi)存泄漏,對(duì)應(yīng)的value在下一次ThreadLocalMap調(diào)用set,get,remove的時(shí)候會(huì)被清除。
因此,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動(dòng)刪除對(duì)應(yīng)key的value就會(huì)導(dǎo)致內(nèi)存泄漏,而不是因?yàn)槿跻谩?/p>
總結(jié)
綜合上面的分析,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果,那么怎么避免內(nèi)存泄漏呢?
每次使用完ThreadLocal,都調(diào)用它的remove()方法,清除數(shù)據(jù)。
在使用線程池的情況下,沒有及時(shí)清理ThreadLocal,不僅是內(nèi)存泄漏的問題,更嚴(yán)重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。
到此這篇關(guān)于淺談ThreadLocal為什么會(huì)內(nèi)存泄漏的文章就介紹到這了,更多相關(guān)ThreadLocal內(nèi)存泄漏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java中 String和StringBuffer的區(qū)別實(shí)例詳解
這篇文章主要介紹了java中 String和StringBuffer的區(qū)別實(shí)例詳解的相關(guān)資料,一個(gè)小的例子,來測(cè)試String和StringBuffer在時(shí)間和空間使用上的差別,需要的朋友可以參考下2017-04-04
java開發(fā)工作中對(duì)InheritableThreadLocal使用思考
這篇文章主要為大家介紹了java開發(fā)工作中對(duì)InheritableThreadLocal使用思考詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
Java發(fā)送http請(qǐng)求的示例(get與post方法請(qǐng)求)
這篇文章主要介紹了Java發(fā)送http請(qǐng)求的示例(get與post方法請(qǐng)求),幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2021-01-01
java使用Feign實(shí)現(xiàn)聲明式Restful風(fēng)格調(diào)用
這篇文章主要為大家詳細(xì)介紹了java使用Feign實(shí)現(xiàn)聲明式Restful風(fēng)格調(diào)用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04
SpringBoot單元測(cè)試之?dāng)?shù)據(jù)隔離詳解
我們?cè)趯憜卧獪y(cè)試時(shí),有一個(gè)比較重要的要求是可以重復(fù)運(yùn)行, 那么這樣就會(huì)有一個(gè)比較麻煩的問題:數(shù)據(jù)污染,所以本文為大家整理了兩個(gè)數(shù)據(jù)隔離的方式,希望對(duì)大家有所幫助2023-08-08
利用Log4j將不同Package的日志輸出到不同文件的方法
日志是應(yīng)用軟件中不可缺少的部分,Apache的開源項(xiàng)目log4j是一個(gè)功能強(qiáng)大的日志組件,提供方便的日志記錄。這篇文章主要介紹了利用Log4j將不同Package的日志輸出到不同文件的方法,需要的朋友可以參考借鑒,下面來跟著小編一起學(xué)習(xí)學(xué)習(xí)吧。2017-01-01
關(guān)于protected修飾符詳解-源于Cloneable接口
這篇文章主要介紹了protected修飾符詳解-源于Cloneable接口,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11

