ThreadLocal系列之——JDK為內(nèi)存泄露做的努力(三)

前情回顧

前文久橙,介紹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ù)探測下一個位置,直到找到一個空閑的位置為止腔剂,該位置就是最終存儲元素的位置媒区,如圖示:

image

回到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是最常用的

image

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)用了它:

image

我們只看到了一個remove方法验辞,與上面說的set\get\remove好像不太對應稿黄?其實只是由于方法分層,沒有直接在set\get里調(diào)用跌造,沒關(guān)系杆怕,由于調(diào)用鏈路比較深族购,筆者使用一張簡單來表示,有興趣的看官們可以拿著手中源碼一起對照著看

image

從上圖中可以看出陵珍,每次調(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ū)別就是有與無了司训!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市液南,隨后出現(xiàn)的幾起案子壳猜,更是在濱河造成了極大的恐慌,老刑警劉巖滑凉,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件统扳,死亡現(xiàn)場離奇詭異,居然都是意外死亡譬涡,警方通過查閱死者的電腦和手機闪幽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來涡匀,“玉大人,你說我怎么就攤上這事溉知≡纱瘢” “怎么了腕够?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長舌劳。 經(jīng)常有香客問我帚湘,道長,這世上最難降的妖魔是什么甚淡? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任大诸,我火速辦了婚禮,結(jié)果婚禮上贯卦,老公的妹妹穿的比我還像新娘资柔。我一直安慰自己,他們只是感情好撵割,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布贿堰。 她就那樣靜靜地躺著,像睡著了一般啡彬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天腐泻,我揣著相機與錄音窄做,去河邊找鬼。 笑死往踢,一個胖子當著我的面吹牛诡渴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播菲语,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼妄辩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了山上?” 一聲冷哼從身側(cè)響起眼耀,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎佩憾,沒想到半個月后哮伟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡妄帘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年楞黄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抡驼。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡鬼廓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出致盟,到底是詐尸還是另有隱情碎税,我是刑警寧澤尤慰,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站雷蹂,受9級特大地震影響伟端,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜匪煌,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一责蝠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧萎庭,春花似錦霜医、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至达舒,卻和暖如春值朋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背巩搏。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工昨登, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贯底。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓丰辣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親禽捆。 傳聞我的和親對象是個殘疾皇子笙什,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容