前情回顧
前文久橙,介紹ThreadLocal不恰當使用姿勢造成的內(nèi)存泄露問題,提醒大家使用完ThreadLocal須記得調(diào)用remove方法及時回收,避免內(nèi)存泄露
誠然雹有,不恰當?shù)氖褂米藙?/strong>確實有內(nèi)存泄露的問題,但JDK作者們?yōu)榱私档蛢?nèi)存泄露對應用程序造成的影響絞盡腦汁臼寄,想盡了辦法霸奕,做了各種各樣的努力和嘗試,本文就來看看作者們做了什么額外的補救措施
這是任何一款成熟的商業(yè)產(chǎn)品該具有的模樣:核心業(yè)務代碼占比應是少部分吉拳,而圍繞在核心代碼之外的大部分代碼都是為了產(chǎn)品體驗更友好质帅,考慮的更周全,能應付各種惡劣的場景)
正文
先回憶一下留攒,內(nèi)存泄露產(chǎn)生的原因:使用了ThreadLocal煤惩,但使用完畢之后未調(diào)用remove方法進行清除
那么本質(zhì)上,本文要探索的問題是:如果確實在未主動調(diào)用remove的情況下造成了內(nèi)存泄露炼邀,JDK做了什么努力來降低這種影響
基本事實
此處先了解一個基本事實:ThreadLocalMap是一個自定義的 hash map魄揉,它與java.util.HashMap在解決hash沖突時所采取的策略不同:ThreadLocalMap使用的是線性探測法來解決沖突,而java.util.HashMap使用的是鏈地址法解決沖突
此處簡單介紹一下線性探測法的原理拭宁。假設(shè)一個數(shù)組長度為n洛退,通過對待放入的Key的hash值對n求余得到要存儲的位置index[index = hash(key)%n],如果該位置已經(jīng)被占用杰标,則令index = index + 1兵怯,繼續(xù)探測下一個位置,直到找到一個空閑的位置為止腔剂,該位置就是最終存儲元素的位置媒区,如圖示:
回到JDK作者解決內(nèi)存泄露的思路,我們來設(shè)想這樣一個場景:
假設(shè)一名古代強盜掸犬,其目的是強劫金銀財寶袜漩,見人就搶風險大收益低,大部分是貧困百姓湾碎,搶了也沒幾個錢宙攻,還冒著暴露的風險;直接上達官貴族家里搶又找不著門路胜茧,或許可能更危險粘优,那怎么辦呢仇味?
強盜們也不笨:此山是我開,此路是我栽雹顺,若要從此過丹墨,留下買路財,選擇直接在通往城鎮(zhèn)的主干道上蹲點嬉愧,見到衣著華貴帶有大批貨物贩挣,攜帶的保鏢兵團也不強的商人,這大概率就是要下手的目標人群没酣。這里的核心邏輯在于:商人們無論進王财、出城貿(mào)易,都需要經(jīng)過城鎮(zhèn)的主干大道裕便,因此在主干道上蹲點能夠【集中兵力】實現(xiàn)效益的最大化
JDK的作者們也是采用了類似的思路:在經(jīng)常被調(diào)用的方法(主干道)上去尋找那些已經(jīng)不使用的臟數(shù)據(jù)并清理掉绒净。這種方式能夠?qū)崿F(xiàn)效益的最大化:使用了最低的成本(僅在少數(shù)方法內(nèi)部去做這件事)解決了最大的問題(內(nèi)存泄露),這也是2\8原則的一種體現(xiàn)
因此偿衰,對于ThreadLocal而言挂疆,需要識別出哪些方法被經(jīng)常調(diào)用的,然后在這些方法上"動手腳"下翎。如下圖示缤言,應該很輕易看出:get、set视事、remove是最常用的
ThreadLocal中清理臟數(shù)據(jù)的方法是java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry
胆萧,如下示:
// java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry
// staleSlot: 待清理的slot,該slot指向的entry一定是stale(舊的)俐东,即key一定是null[ThreadLocal.get()為null]
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// ...(省略)
return i;
}
入?yún)ⅲ簊taleSlot跌穗,待清理的slot,該slot指向的entry一定stale(舊的)犬性,即key一定是null[ThreadLocal.get()為null]
畫外音:該方法只專注于理清Entry這件事瞻离,而不管Entry的內(nèi)容實際上是否舊的腾仅,因此需要由調(diào)用方來保證staleSlot指向的entry一定stale
代碼邏輯:
將entry里值的強引用置為null乒裆,即斷掉指向值對象的強引用(這樣值對象就對被GC回收)
將entry對應引用置為null,即斷掉指向Entry對象的強引用(這樣Entry就能被GC回收)
因此推励,只要調(diào)用expungeStaleEntry方法鹤耍,就能將無用entry(臟數(shù)據(jù))回收清理掉。現(xiàn)在來看看哪些方法調(diào)用了它:
我們只看到了一個remove方法验辞,與上面說的set\get\remove好像不太對應稿黄?其實只是由于方法分層,沒有直接在set\get里調(diào)用跌造,沒關(guān)系杆怕,由于調(diào)用鏈路比較深族购,筆者使用一張簡單來表示,有興趣的看官們可以拿著手中源碼一起對照著看
從上圖中可以看出陵珍,每次調(diào)用set\get\remove寝杖,確實最終會調(diào)用到expungeStaleEntry方法并將無用entry(臟數(shù)據(jù))回收清理掉
方法眾多,筆者挑一個相對有意思的cleanSomeSlots
進行分析
方法簽名為private boolean cleanSomeSlots(int i, int n)
互纯,從方法名稱上看瑟幕,"清理一些slots" 意味著它會清理slot,但很有可能只會清理一些而不是清理所有留潦,因此"一些"是作者刻意為之只盹,巧妙地設(shè)計成對數(shù)次的掃描清理,這是設(shè)計上的一個Trade Off :
如果不清理兔院,方法誠然很快就能完成并返回殖卑,但是這就意味著內(nèi)存泄露,意味著臟垃圾
如果全清理坊萝,每次插入都伴隨著entry數(shù)組的全掃描懦鼠,隨著數(shù)組的擴容,put方法性能會持續(xù)惡化
因此屹堰,需要找到完全不掃描與slot數(shù)量成正比的掃描次數(shù)之間的平衡肛冶,即O(1)與O(n)之間的平衡,學過數(shù)據(jù)結(jié)構(gòu)的同學應該很輕易聯(lián)想到O(log2[n])扯键,不會隨著數(shù)組的擴容而導致掃描效率變低睦袖。實現(xiàn)細節(jié)如下:
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); // 右移1位,相當于除以2
return removed;
}
至此荣刑,看官們是否有一種似曾相識的感覺馅笙,腦瓜中隱隱約約回憶起某個組件有過類似場景的處理:Redis的過期淘汰策略
對于Redis過期的Key,采用的是定期刪除 + 惰性刪除 機制
定期刪除: Redis每隔一定的時間厉亏,就抽取"一批"而不是“所有”過期的Key進行刪除董习,通過控制刪除Key的數(shù)量、執(zhí)行間隔來減少CPU的消耗
惰性刪除: Redis在獲取某個Key時爱只,檢查一下是否過期皿淋,如果已過期就執(zhí)行刪除操作并返回空
其中的定期刪除所采用的思想與cleanSomeSlots如出一轍:都是選取一批而不是全部的Key來進行刪除,以此來權(quán)衡內(nèi)存占用與CPU占用之間的關(guān)系
你瞧恬试,大牛們思想是一致的窝趣,技術(shù)的本質(zhì)都是相通的!
總結(jié)
本文主要是表達了一個觀點:ThreadLocal不恰當使用姿勢盡管容易造成內(nèi)存泄露训柴,但是做為一個成熟的產(chǎn)品哑舒,作者有努力去解決這個問題,已經(jīng)盡可能地把問題造成的影響最小化幻馁。在這個過程中洗鸵,我們還發(fā)現(xiàn)了一個設(shè)計上的Trade Off越锈,并且思想上與Redis的過期淘汰策略一致(知識的遷移),如果大家能從中獲得一些啟發(fā)膘滨,將會是看官們閱讀本文最大的收獲瞪浸,至少筆者本人是這樣
畫外音
常常會看到這樣的簡歷:閱讀過XXX源碼。嗯吏祸,是的对蒲,閱讀過,然后呢贡翘?有沒有得到一些啟發(fā)呢蹈矮,思想上有沒有更進步呢?大家都閱讀同一份源碼鸣驱,為什么有人就能從中獲得巨大的知識泛鸟,而自己卻云里霧里?筆者認為踊东,閱讀一款優(yōu)秀的開源框架代碼北滥,如果深陷代碼細節(jié)泥淖中而未曾GET到作者的思想,可能是看了個寂寞闸翅。并非細節(jié)不重要再芋,而是相比于具體怎么實現(xiàn)的細節(jié),思想上的收獲可能來的更有用一些:一旦掌握思想坚冀,你也大概率能寫出來類似功能的代碼济赎,區(qū)別僅在于好與壞;如果連思想都未曾學到记某,區(qū)別就是有與無了司训!