內(nèi)存泄露的原因找到了藕筋,罪魁禍?zhǔn)拙尤皇荍ava TheadLocal

ThreadLocal使用不規(guī)范纵散,師傅兩行淚

組內(nèi)來了一個(gè)實(shí)習(xí)生梳码,看這小伙子春光滿面隐圾、精神抖擻、頭發(fā)微少掰茶,我心頭一喜:絕對(duì)是個(gè)潛力股暇藏。于是我找經(jīng)理申請(qǐng)親自來帶他,為了幫助小伙子快速成長(zhǎng)濒蒋,我給他分了一個(gè)需求盐碱,這不需求剛上線幾天就出網(wǎng)上問題了??后臺(tái)監(jiān)控服務(wù)發(fā)現(xiàn)內(nèi)存一直在緩慢上升,初步懷疑是內(nèi)存泄露沪伙。

把實(shí)習(xí)生的PR都找出來仔細(xì)review瓮顽,果然發(fā)現(xiàn)問題了。由于公司內(nèi)部代碼是保密的围橡,這里簡(jiǎn)單寫一個(gè)demo還原場(chǎng)景(忽略代碼風(fēng)格問題)暖混。

public class ThreadPoolDemo {
    private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; ++i) {
            poolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();
                    threadLocal.set(new BigObject());
                    // 其他業(yè)務(wù)代碼
                }
            });
            Thread.sleep(1000);
        }
    }
    static class BigObject {
        // 100M
        private byte[] bytes = new byte[100 * 1024 * 1024];
    }
}

代碼分析:

  • 創(chuàng)建一個(gè)核心線程數(shù)和最大線程數(shù)都為10的線程池,保證線程池里一直會(huì)有10個(gè)線程在運(yùn)行翁授。
  • 使用for循環(huán)向線程池中提交了100個(gè)任務(wù)拣播。
  • 定義了一個(gè)ThreadLocal類型的變量,Value類型是大對(duì)象收擦。
  • 每個(gè)任務(wù)會(huì)向threadLocal變量里塞一個(gè)大對(duì)象贮配,然后執(zhí)行其他業(yè)務(wù)邏輯。
  • 由于沒有調(diào)用線程池的shutdown方法塞赂,線程池里的線程還是會(huì)在運(yùn)行泪勒。

乍一看這代碼好像沒有什么問題,那為什么會(huì)導(dǎo)致服務(wù)GC后內(nèi)存還高居不下呢?

代碼中給threadLocal賦值了一個(gè)大的對(duì)象酣藻,但是執(zhí)行完業(yè)務(wù)邏輯后沒有調(diào)用remove方法曹洽,最后導(dǎo)致線程池中10個(gè)線程的threadLocals變量中包含的大對(duì)象沒有被釋放掉,出現(xiàn)了內(nèi)存泄露辽剧。

大家說說這樣的實(shí)習(xí)生還能留不送淆?

ThreadLocal的value值存在哪里?

實(shí)習(xí)生說他以為線程任務(wù)結(jié)束了threadLocal賦值的對(duì)象會(huì)被JVM垃圾回收怕轿,很疑惑為什么會(huì)出現(xiàn)內(nèi)存泄露偷崩。作為師傅我肯定要給他把原理講透呀。

ThreadLocal類提供set/get方法存儲(chǔ)和獲取value值撞羽,但實(shí)際上ThreadLocal類并不存儲(chǔ)value值阐斜,真正存儲(chǔ)是靠ThreadLocalMap這個(gè)類,ThreadLocalMapThreadLocal的一個(gè)靜態(tài)內(nèi)部類诀紊,它的keyThreadLocal實(shí)例對(duì)象谒出,value是任意Object對(duì)象。

ThreadLocalMap類的定義

static class ThreadLocalMap {
    // 定義一個(gè)table數(shù)組邻奠,存儲(chǔ)多個(gè)threadLocal對(duì)象及其value值
    private Entry[] table;
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
    }
    // 定義一個(gè)Entry類笤喳,key是一個(gè)弱引用的ThreadLocal對(duì)象
    // value是任意對(duì)象
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    // 省略其他
}

進(jìn)一步分析ThreadLocal類的代碼,看setget方法如何與ThreadLocalMap靜態(tài)內(nèi)部類關(guān)聯(lián)上碌宴。

ThreadLocal類set方法

public class ThreadLocal<T> {
 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    // 省略其他方法
}

set的邏輯比較簡(jiǎn)單杀狡,就是獲取當(dāng)前線程的ThreadLocalMap,然后往map里添加KV贰镣,K是當(dāng)前ThreadLocal實(shí)例呜象,V是我們傳入的value。這里需要注意一下碑隆,map的獲取是需要從Thread類對(duì)象里面取恭陡,看一下Thread類的定義。

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    //省略其他
}

Thread類維護(hù)了一個(gè)ThreadLocalMap的變量引用上煤。

ThreadLocal類get方法

get獲取當(dāng)前線程的對(duì)應(yīng)的私有變量休玩,是之前set或者通過initialValue的值,代碼如下:

class ThreadLocal<T> {
    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();
    }
}

代碼邏輯分析:

  • 獲取當(dāng)前線程的ThreadLocalMap實(shí)例楼入;
  • 如果不為空哥捕,以當(dāng)前ThreadLocal實(shí)例為key獲取value
  • 如果ThreadLocalMap為空或者根據(jù)當(dāng)前ThreadLocal實(shí)例獲取的value為空嘉熊,則執(zhí)行setInitialValue()遥赚;

ThreadLocal相關(guān)類的關(guān)系總結(jié)

看了上面的分析是不是對(duì)ThreadThreadLocal阐肤,ThreadLocalMap凫佛,Entry這幾個(gè)類之間的關(guān)系有點(diǎn)暈了讲坎,沒關(guān)系我專門畫了一個(gè)UML類圖來總結(jié)(忽略UML標(biāo)準(zhǔn)語法)。

image
  • 每個(gè)線程是一個(gè)Thread實(shí)例愧薛,其內(nèi)部維護(hù)一個(gè)threadLocals的實(shí)例成員晨炕,其類型是ThreadLocal.ThreadLocalMap
  • 通過實(shí)例化ThreadLocal實(shí)例毫炉,我們可以對(duì)當(dāng)前運(yùn)行的線程設(shè)置一些線程私有的變量瓮栗,通過調(diào)用ThreadLocalsetget方法存取。
  • ThreadLocal本身并不是一個(gè)容器瞄勾,我們存取的value實(shí)際上存儲(chǔ)在ThreadLocalMap中费奸,ThreadLocal只是作為TheadLocalMapkey
  • 每個(gè)線程實(shí)例都對(duì)應(yīng)一個(gè)TheadLocalMap實(shí)例进陡,我們可以在同一個(gè)線程里實(shí)例化很多個(gè)ThreadLocal來存儲(chǔ)很多種類型的值愿阐,這些ThreadLocal實(shí)例分別作為key,對(duì)應(yīng)各自的value趾疚,最終存儲(chǔ)在Entry table數(shù)組中缨历。
  • 當(dāng)調(diào)用ThreadLocalset/get進(jìn)行賦值/取值操作時(shí),首先獲取當(dāng)前線程的ThreadLocalMap實(shí)例糙麦,然后就像操作一個(gè)普通的map一樣辛孵,進(jìn)行putget

ThreadLocal內(nèi)存模型原理

經(jīng)過上面的分析我們對(duì)ThreadLocal相關(guān)的類設(shè)計(jì)已經(jīng)非常清楚了喳资,下面通過一張圖更加深入理解一下ThreadLocal的內(nèi)存存儲(chǔ)觉吭。

image

圖中左邊是棧腾供,右邊是堆仆邓。線程的一些局部變量和引用使用的內(nèi)存屬于Stack(棧)區(qū),而普通的對(duì)象是存儲(chǔ)在Heap(堆)區(qū)伴鳖。

  • 線程運(yùn)行時(shí)节值,我們定義的TheadLocal對(duì)象被初始化,存儲(chǔ)在Heap榜聂,同時(shí)線程運(yùn)行的棧區(qū)保存了指向該實(shí)例的引用搞疗,也就是圖中的ThreadLocalRef
  • 當(dāng)ThreadLocalset/get被調(diào)用時(shí)须肆,虛擬機(jī)會(huì)根據(jù)當(dāng)前線程的引用也就是CurrentThreadRef找到其對(duì)應(yīng)在堆區(qū)的實(shí)例匿乃,然后查看其對(duì)用的TheadLocalMap實(shí)例是否被創(chuàng)建,如果沒有豌汇,則創(chuàng)建并初始化幢炸。
  • Map實(shí)例化之后,也就拿到了該ThreadLocalMap的句柄拒贱,那么就可以將當(dāng)前ThreadLocal對(duì)象作為key宛徊,進(jìn)行存取操作佛嬉。
  • 圖中的虛線,表示key對(duì)應(yīng)ThreadLocal實(shí)例的引用是個(gè)弱引用闸天。

強(qiáng)引用弱引用的概念

ThreadLocalMapkey是一個(gè)弱引用類型暖呕,源代碼如下:

static class ThreadLocalMap {
    // 定義一個(gè)Entry類,key是一個(gè)弱引用的ThreadLocal對(duì)象
    // value是任意對(duì)象
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    // 省略其他
}

下面解釋一下常見的幾種引用概念苞氮。

強(qiáng)引用

一直活著:類似“Object obj=new Object()”這類的引用湾揽,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象實(shí)例笼吟。

弱引用

回收就會(huì)死亡:被弱引用關(guān)聯(lián)的對(duì)象實(shí)例只能生存到下一次垃圾收集發(fā)生之前钝腺。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠赞厕,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象實(shí)例艳狐。在JDK 1.2之后,提供了WeakReference類來實(shí)現(xiàn)弱引用皿桑。

軟引用

有一次活的機(jī)會(huì):軟引用關(guān)聯(lián)著的對(duì)象毫目,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象實(shí)例列進(jìn)回收范圍之中進(jìn)行第二次回收诲侮。如果這次回收還沒有足夠的內(nèi)存镀虐,才會(huì)拋出內(nèi)存溢出異常。在JDK 1.2之后沟绪,提供了SoftReference類來實(shí)現(xiàn)軟引用刮便。

虛引用

也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系绽慈。一個(gè)對(duì)象實(shí)例是否有虛引用的存在恨旱,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對(duì)象實(shí)例坝疼。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象實(shí)例被收集器回收時(shí)收到一個(gè)系統(tǒng)通知搜贤。在JDK 1.2之后,提供了PhantomReference類來實(shí)現(xiàn)虛引用钝凶。

內(nèi)存泄露是不是弱引用的鍋仪芒?

從表面上看內(nèi)存泄漏的根源在于使用了弱引用,但是另一個(gè)問題也同樣值得思考:為什么ThreadLocalMap使用弱引用而不是強(qiáng)引用耕陷?

翻看官網(wǎng)文檔的說法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了處理非常大和長(zhǎng)期的用途掂名,哈希表?xiàng)l目使用weakreference作為鍵。

分兩種情況討論:
(1)key 使用強(qiáng)引用
引用ThreadLocal的對(duì)象被回收了哟沫,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用饺蔑,如果沒有手動(dòng)刪除,ThreadLocal不會(huì)被回收南用,導(dǎo)致Entry內(nèi)存泄漏膀钠。
(2)key 使用弱引
引用ThreadLocal的對(duì)象被回收了掏湾,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動(dòng)刪除肿嘲,ThreadLocal也會(huì)被回收融击。value在下一次ThreadLocalMap調(diào)用set、get雳窟、remove的時(shí)候會(huì)被清除尊浪。

比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng)封救,如果都沒有手動(dòng)刪除對(duì)應(yīng)key拇涤,都會(huì)導(dǎo)致內(nèi)存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal被清理后keynull誉结,對(duì)應(yīng)的value在下一次ThreadLocalMap調(diào)用set鹅士、get、remove的時(shí)候可能會(huì)被清除惩坑。

因此掉盅,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果沒有手動(dòng)刪除對(duì)應(yīng)key就會(huì)導(dǎo)致內(nèi)存泄漏以舒,而不是因?yàn)槿跻谩?/p>

ThreadLocal最佳實(shí)踐

通過前面幾小節(jié)我們分析了ThreadLocal的類設(shè)計(jì)以及內(nèi)存模型趾痘,同時(shí)也重點(diǎn)分析了發(fā)生內(nèi)存泄露的條件和特定場(chǎng)景。最后結(jié)合項(xiàng)目中的經(jīng)驗(yàn)給出建議使用ThreadLocal的場(chǎng)景:

  • 當(dāng)需要存儲(chǔ)線程私有變量的時(shí)候蔓钟。
  • 當(dāng)需要實(shí)現(xiàn)線程安全的變量時(shí)永票。
  • 當(dāng)需要減少線程資源競(jìng)爭(zhēng)的時(shí)候。

綜合上面的分析滥沫,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果侣集,那么怎么避免內(nèi)存泄漏呢?

答案就是:每次使用完ThreadLocal佣谐,建議調(diào)用它的remove()方法肚吏,清除數(shù)據(jù)方妖。

另外需要強(qiáng)調(diào)的是并不是所有使用ThreadLocal的地方狭魂,都要在最后remove(),因?yàn)樗麄兊纳芷诳赡苁切枰晚?xiàng)目的生存周期一樣長(zhǎng)的党觅,所以要進(jìn)行恰當(dāng)?shù)倪x擇雌澄,以免出現(xiàn)業(yè)務(wù)邏輯錯(cuò)誤!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末杯瞻,一起剝皮案震驚了整個(gè)濱河市镐牺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌魁莉,老刑警劉巖睬涧,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件募胃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡畦浓,警方通過查閱死者的電腦和手機(jī)痹束,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來讶请,“玉大人祷嘶,你說我怎么就攤上這事《嵋纾” “怎么了论巍?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)风响。 經(jīng)常有香客問我嘉汰,道長(zhǎng),這世上最難降的妖魔是什么状勤? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任郑现,我火速辦了婚禮,結(jié)果婚禮上荧降,老公的妹妹穿的比我還像新娘接箫。我一直安慰自己,他們只是感情好朵诫,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布辛友。 她就那樣靜靜地躺著,像睡著了一般剪返。 火紅的嫁衣襯著肌膚如雪废累。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天脱盲,我揣著相機(jī)與錄音邑滨,去河邊找鬼。 笑死钱反,一個(gè)胖子當(dāng)著我的面吹牛掖看,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播面哥,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼哎壳,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了尚卫?” 一聲冷哼從身側(cè)響起归榕,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吱涉,沒想到半個(gè)月后刹泄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體外里,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年特石,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了级乐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡县匠,死狀恐怖风科,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情乞旦,我是刑警寧澤贼穆,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布兰粉,位于F島的核電站故痊,受9級(jí)特大地震影響玖姑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜焰络,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望闪彼。 院中可真熱鬧,春花似錦畏腕、人聲如沸缴川。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至铭污,卻和暖如春恋日,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背况凉。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工谚鄙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人刁绒。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像烤黍,于是被迫代替她去往敵國(guó)和親知市。 傳聞我的和親對(duì)象是個(gè)殘疾皇子傻盟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348