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