偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

ThreadLocal源碼解讀:內(nèi)存泄露問題分析

開發(fā) 前端
ThreadLocal 優(yōu)勢是無鎖化提升并發(fā)性能和簡化變量的傳遞邏輯。在實際業(yè)務(wù)中使用 ThreadLocal 類時應(yīng)該在恰當(dāng)位置調(diào)用 remove 方法顯式移除值。盡可能的避免觸發(fā) ThreadLocal 清理過時 Entry 的邏輯,從而提高 ThreadLocal 性能。

引言

大家好,我們又見面了。今天依舊是結(jié)合源碼為大家分享個人對于 ThreadLocal 的一些理解。今天是第二期,將著重分析 ThreadLocal 內(nèi)存泄露問題,文章后半篇含重點源碼精講,不容錯過。廢話不多說,坐穩(wěn)發(fā)車咯!

上期回顧

在上一期,我通過閱讀源碼的方式帶大家學(xué)習(xí)了 ThreadLocal 常用的 API,并在這個過程中深度剖析了 ThreadLocal 的存儲結(jié)構(gòu)。

下面通過我剛剛繪制的一張圖來為大家回顧一下上一節(jié)所闡述的存儲結(jié)構(gòu)。

圖片圖片

如果大家對這個存儲結(jié)構(gòu)有所疑惑,可以回看第一期《ThreadLocal 源碼解讀:初識 ThreadLocal》。

引用類型

在 Java 中有四種常用的引用類型,依照引用的強(qiáng)弱排序依次是:強(qiáng)引用、軟引用、弱引用、幻引用(虛引用)。

其中強(qiáng)引用就是我們通常所說的引用,所以這里 Java 并沒有單獨定義一個引用類來表示,并且強(qiáng)引用存在時被引用對象一定不會被垃圾回收器回收。

軟引用在 Java 中使用 SoftReference 類表示,被軟引用單獨引用的對象當(dāng)系統(tǒng)內(nèi)存不足的時候會被垃圾回收器所回收,也就是說在發(fā)生 OOM 前將會回收軟引用對象,試圖避免 OOM 的發(fā)生。

弱引用在 Java 中使用 WeakReference 類表示,被弱引用單獨引用的對象在發(fā)生任意垃圾回收時,無論內(nèi)存是否充足都將會被回收。

幻引用在 Java 中使用 PhantomReference 類表示,是最弱的引用類型,主要用于跟蹤對象是否被垃圾回收,并且幻引用的 get 方法永遠(yuǎn)返回 null。

上述三種引用類均繼承 Reference 類,Reference 類通過泛型成員變量 referent 存儲引用對象,并提供了 get 方法用于獲取引用對象,提供 clear 方法用于清理引用對象。

圖片圖片

內(nèi)存泄露問題剖析

拋出觀點

在探究 ThreadLocal 內(nèi)存泄漏問題之前,我們首先要明確一下,什么是內(nèi)存泄露?

這里我們直接引用百度百科提供的答案。


內(nèi)存泄漏(Memory Leak)是指程序中已動態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費,導(dǎo)致程序運行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。

那么 ThreadLocal 在使用過程中存在泄露問題嗎?答案是肯定的,但是要糾正一點,ThreadLocal 的內(nèi)存泄露問題與 ThreadLocal 對象的弱引用并無關(guān)系!這一點在網(wǎng)上可能存在著誤導(dǎo)信息,下面將會為大家論證我的觀點。

推理驗證

首先我們來看一下 Entry 類的定義。

圖片圖片

可以看到 Entry 類繼承了 WeakReference 類,并且將弱引用的 ThreadLocal 對象作為了 ThreadLocalMap 的鍵。

查閱過 ThreadLocal 相關(guān)博客的小伙伴可能看過下面種說法。

--start--

ThreadLocal 變量如果未被正確清理,可能會導(dǎo)致內(nèi)存泄露。因為 ThreadLocalMap 的鍵是 ThreadLocal 對象的弱引用,值是強(qiáng)引用。

當(dāng) ThreadLocal 對象不再被外部引用時,ThreadLocalMap 中的鍵會被垃圾回收,但值仍然存在,導(dǎo)致無法被垃圾回收,從而引發(fā)內(nèi)存泄露。

--end--

在這個過程中的確存在內(nèi)存泄露問題,但這和 ThreadLocalMap 的 key 設(shè)計并無關(guān)系,這是編寫程序的不嚴(yán)謹(jǐn)導(dǎo)致的問題,在使用完 ThreadLocal 后,沒有調(diào)用 remove 方法顯式移除值。

任何一個 Java 對象都可能因為使用不當(dāng)導(dǎo)致內(nèi)存泄漏,比如聲明了一個類的對象用作成員變量,但是卻從未在代碼里使用過這個成員變量(如下圖),這也是內(nèi)存泄漏。

圖片圖片

所以并不是因為 ThreadLocalMap 的 key 的弱引用設(shè)計,才導(dǎo)致的內(nèi)存泄露問題。恰恰相反,ThreadLocalMap 的 key 的弱引用設(shè)計一定程度上減少了內(nèi)存泄露的損失。

首先當(dāng) ThreadLocalMap 的 key 不再被外部所引用時,ThreadLocal 對象以及通過 ThreadLocal 存儲在 ThreadLocalMap 中的值已經(jīng)無法在其他地方被獲取,已經(jīng)發(fā)生了內(nèi)存泄漏。那么這時候垃圾回收器回收掉 ThreadLocalMap 的 key,恰恰為我們釋放了一部分已經(jīng)泄露的內(nèi)存。

這時候有人可能會有疑問,那 value 就不管了嗎?當(dāng)然不是!雖然這是開發(fā)者 API 使用不當(dāng)留下的坑,但是設(shè)計者也為我們填了這個坑。

注意看 Entry 類的注釋,這里我直接為大家翻譯出來。

圖片圖片

可以看到官方將 key 為 null 的 Entry 對象稱之為“陳舊條目”,也就是我上一期文章所說的過時 Entry,并且官方指出這些過時 Entry 可以從 ThreadLocalMap 中刪除。

那么不難猜到,ThreadLocal 在設(shè)計時一定在某些時機(jī)對這些過時 Entry 進(jìn)行了清理,盡可能的釋放泄露的內(nèi)存。

這里先給出大家結(jié)論,然后我們再去論證:ThreadLocal在調(diào)用set(),get(),remove()方法的時候,都可能觸發(fā)清理過時Entry的邏輯。。

清理方法源碼剖析

expungeStaleEntry 方法

在討論到 ThreadLocalMap 過時 Entry 清理的問題,就繞不開 ThreadLocalMap 的 expungeStaleEntry 這個方法,見名之意這個方法用于刪除過時 Entry。

下面我將采用在源碼中添加注釋的方式剖析這個方法。

/**
 * Expunge a stale entry by rehashing any possibly colliding entries
 * lying between staleSlot and the next null slot.  This also expunges
 * any other stale entries encountered before the trailing null.  See
 * Knuth, Section 6.4
 *
 * @param staleSlot index of slot known to have null key
 * @return the index of the next null slot after staleSlot
 * (all between staleSlot and this slot will have been checked
 * for expunging).
 */
private int expungeStaleEntry(int staleSlot) {
    // 入?yún)?staleSlot: 待清理位置下標(biāo)
    
    // 獲取 ThreadLocalMap 中的 Entry 數(shù)組。
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    // len 為當(dāng)前 Entry 數(shù)組容量。
    int len = tab.length;

    // expunge entry at staleSlot
    // 清除當(dāng)前 staleSlot 位置的過時 Entry。
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    // 元素數(shù)量減一。
    size--;

    // Rehash until we encounter null
    // 因為 ThreadLocalMap 解決哈希沖突采用的是線性探測法,如將當(dāng)前下標(biāo)位置賦值為 null ,但不對后續(xù) Entry
    // 元素進(jìn)行 rehash 操作,就可能導(dǎo)致存在哈希沖突的后置元素?zé)o法被探測到。所以將當(dāng)前元素清理后需要
    // 對后續(xù)元素進(jìn)行 rehash 操作,直到遇到下一個為 null 的元素。
    ThreadLocal.ThreadLocalMap.Entry e;
    int i;
    // nextIndex 用于向后遞增索引 ((i + 1 < len) ? i + 1 : 0)
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 此時 Entry 不為 null,key 為 null,Entry 為過時 Entry 需清理掉。
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 此時為有效 Entry,需要進(jìn)行 rehash 操作重新定位。
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                // 進(jìn)入到這個分支說明 rehash 后,新的下標(biāo)與原來下標(biāo)不等。
                // 將當(dāng)前下標(biāo)位置清空。
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                // 從 h 位置開始遍歷,直到遇到為 null 的元素,并將 rehash 后的元素插入到該位置。
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    
    // i 為 staleSlot 后的第一個 null 元素的位置下標(biāo)。
    return i;
}

了解了 expungeStaleEntry 方法的內(nèi)部實現(xiàn)細(xì)節(jié)之后就可以把這個方法當(dāng)做一個黑盒,作用是清理傳入下標(biāo)位置的過時 Entry,入?yún)橐粋€過時 Entry 的下標(biāo)。

cleanSomeSlots 方法

有了 expungeStaleEntry 方法的基礎(chǔ)我們就可以攻克下一個和清理過時 Entry 相關(guān)的方法:cleanSomeSlots。見名之意,這個方法的作用是清除一些過時 Entry。

同樣采用在源碼中添加注釋的方式剖析這個方法。

/**
 * Heuristically scan some cells looking for stale entries.
 * This is invoked when either a new element is added, or
 * another stale one has been expunged. It performs a
 * logarithmic number of scans, as a balance between no
 * scanning (fast but retains garbage) and a number of scans
 * proportional to number of elements, that would find all
 * garbage but would cause some insertions to take O(n) time.
 *
 * @param i a position known NOT to hold a stale entry. The
 * scan starts at the element after i.
 *
 * @param n scan control: {@code log2(n)} cells are scanned,
 * unless a stale entry is found, in which case
 * {@code log2(table.length)-1} additional cells are scanned.
 * When called from insertions, this parameter is the number
 * of elements, but when from replaceStaleEntry, it is the
 * table length. (Note: all this could be changed to be either
 * more or less aggressive by weighting n instead of just
 * using straight log n. But this version is simple, fast, and
 * seems to work well.)
 *
 * @return true if any stale entries have been removed.
 */
private boolean cleanSomeSlots(int i, int n) {
    // 入?yún)?i: 一個已知不為過時 Entry 的下標(biāo)。掃描從 i 之后的位置開始。
    // 入?yún)?n: 掃描次數(shù)控制值
    
    // 是否清理了任意過時 Entry 標(biāo)志,
    // 為 false 代表本次方法調(diào)用未能清理任何過時 Entry,為 true 代表本次方法調(diào)用至少清理了一個過時 Entry。
    boolean removed = false;
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    // doWhile 循環(huán),至少執(zhí)行一次。
    do {
        i = nextIndex(i, len);
        ThreadLocal.ThreadLocalMap.Entry e = tab[i];
        if (e != null && e.get() == null) {
            // 進(jìn)入到當(dāng)前分支說明當(dāng)前 Entry 為過時 Entry。
            // 將 n 置為數(shù)組容量,將當(dāng)前循環(huán)遍歷次數(shù)進(jìn)行追增(n 變大了)。
            n = len;
            // 將標(biāo)志置為 true,證明本次方法調(diào)用并不是無功而返。
            removed = true;
            // 調(diào)用清理過時 Entry 方法,并將 expungeStaleEntry 方法返回的 null 元素的下標(biāo)賦值給 i,
            // 在這之間的下標(biāo)都在 expungeStaleEntry 方法中進(jìn)行了清理,所以這里直接跳過避免重復(fù)操作。
            i = expungeStaleEntry(i);
        }
        // >>>= 無符號右移并賦值,相當(dāng)于除以 2 操作。
    } while ( (n >>>= 1) != 0);
    // 返回本次是否至少清理了一個過時 Entry。
    return removed;
}

cleanSomeSlots 方法在 set 和 remove 方法調(diào)用中會被調(diào)用到,這個方法在完全不掃描以及全量掃描中做了一個平衡,采用以對數(shù)的方式進(jìn)行掃描,并且如果發(fā)現(xiàn)了過時 Entry 則會再追增對數(shù)次掃描,使得在保證 set 方法和 remove 方法的執(zhí)行效率的情況下一定程度上清理了過時 Entry。

replaceStaleEntry 方法

下面我們來看一下最后一個與清理過時 key 有關(guān)的方法:replaceStaleEntry,通過方法名我們可以推測出這個方法的作用是替換過時條目,那么用什么替換呢,是 set 方法傳過來的 Entry。

同樣采用在源碼中添加注釋的方式剖析這個方法,這個方法有些許難度,如果大家不理解,可以多閱讀幾遍。

/**
 * Replace a stale entry encountered during a set operation
 * with an entry for the specified key.  The value passed in
 * the value parameter is stored in the entry, whether or not
 * an entry already exists for the specified key.
 *
 * As a side effect, this method expunges all stale entries in the
 * "run" containing the stale entry.  (A run is a sequence of entries
 * between two null slots.)
 *
 * @param  key the key
 * @param  value the value to be associated with key
 * @param  staleSlot index of the first stale entry encountered while
 *         searching for key.
 */
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    // 入?yún)?key: set 操作傳過來的 key
    // 入?yún)?value: set 操作傳過來的 value,與參數(shù) key 相關(guān)聯(lián)
    // 入?yún)?staleSlot: 待替換的過時 Entry 的下標(biāo)
    
    ThreadLocal.ThreadLocalMap.Entry[] tab = table;
    int len = tab.length;
    ThreadLocal.ThreadLocalMap.Entry e;

    // Back up to check for prior stale entry in current run.
    // We clean out whole runs at a time to avoid continual
    // incremental rehashing due to garbage collector freeing
    // up refs in bunches (i.e., whenever the collector runs).
    // slotToExpunge 變量目標(biāo)是存儲當(dāng)前區(qū)間段(兩個 null 元素之間),第一個過時 Entry 的下標(biāo)。
    // 將入?yún)⒌倪^時 Entry 下標(biāo)賦值給 slotToExpunge。
    int slotToExpunge = staleSlot;
    // 這里需要格外注意一下,這里并不是遞增下標(biāo),而是對下標(biāo)進(jìn)行遞減。
    // prevIndex ((i - 1 >= 0) ? i - 1 : len - 1)。
    // 向前進(jìn)行遍歷直到遇到為 null 的元素。
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            // 將遍歷過程中過時 Entry 的下標(biāo)賦值給 slotToExpunge 變量。
            // 經(jīng)過當(dāng)前遍歷邏輯,slotToExpunge 將存儲兩個 null 元素之間第一個過時 key 的下標(biāo)。
            slotToExpunge = i;

    // Find either the key or trailing null slot of run, whichever
    // occurs first
    // 這里是由入?yún)⒌倪^時 Entry 下標(biāo)開始向后遍歷,直到遇到 null 元素。
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // If we find key, then we need to swap it
        // with the stale entry to maintain hash table order.
        // The newly stale slot, or any other stale slot
        // encountered above it, can then be sent to expungeStaleEntry
        // to remove or rehash all of the other entries in run.
        if (k == key) {
            // 進(jìn)入當(dāng)前分支表示在遍歷的過程中找到了被 set 的 Entry 對象的本體。
            // 將和 key 關(guān)聯(lián)的新 value 值賦值給本體。
            e.value = value;
            // 操作一
            // 將 Entry 對象本體和入?yún)?staleSlot 位置的過時 Entry 進(jìn)行交換,
            // 結(jié)果是set操作的 key 與 value,無論之前本體存儲在哪里,
            // 最終都會存儲在入?yún)⒌?staleSlot 下標(biāo),符合方法名中的 replace 含義。
            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                // 如果當(dāng)前區(qū)間段第一個過時 Entry 下標(biāo)仍是 staleSlot 下標(biāo),
                // 那么需要將當(dāng)前 i 下標(biāo)賦值給 slotToExpunge ,因為 staleSlot 下標(biāo)已經(jīng)存儲了 set 操作的 Entry 對象,
                // 導(dǎo)致當(dāng)前 i 下標(biāo)變成了第一個過時 Entry 的下標(biāo)。
                slotToExpunge = i;
            // 先調(diào)用 expungeStaleEntry 方法清除 slotToExpunge 下標(biāo)的過時 Entry,
            // 再從 expungeStaleEntry 方法返回的 null 元素的下標(biāo)開始執(zhí)行 cleanSomeSlots 方法。
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            // 已經(jīng)完成替換過時條目操作,退出當(dāng)前方法。
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            // 進(jìn)入當(dāng)前分支說明當(dāng)前 Entry 是過時 Entry。
            // 如果當(dāng)前區(qū)間段第一個過時 Entry 下標(biāo)仍是入?yún)⒌?staleSlot 下標(biāo),
            // 則需要將當(dāng)前位置下標(biāo)賦值給 slotToExpunge,因為最終當(dāng)前位置的過時 Entry 將是
            // 當(dāng)前區(qū)間段的第一個過時 Entry。因為 staleSlot 下標(biāo)位置的過時 Entry 在之后的邏輯
            // 里要么被交換到當(dāng)前下標(biāo)之后(上文操作一),要么被新的 set 傳入的 Entry 覆蓋掉(下文操作二)。
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    // 操作二
    // 代碼執(zhí)行到當(dāng)前位置說明 set 的 key 與 value 是一個新的 Entry,在之前并不存在。
    // 以 set 方法傳入的 key 和 value 值 new 一個新的 Entry 對象,并覆蓋在入?yún)⒌?staleSlot 下標(biāo)處。
    tab[staleSlot].value = null;
    tab[staleSlot] = new ThreadLocal.ThreadLocalMap.Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        // slotToExpunge 與 staleSlot 相等則說明當(dāng)前區(qū)間段只有入?yún)?staleSlot 位置有過時 Entry,
        // 并且該過時 Entry 已被覆蓋,所以無需清理,無需進(jìn)入當(dāng)前分支。
        
        // 進(jìn)入當(dāng)前分支說明當(dāng)前區(qū)間段,除了被覆蓋的過時 Entry,至少還存在一個過時 Entry,
        // slotToExpunge 下標(biāo)為第一個過時 Entry 的下標(biāo)。
        // 先調(diào)用 expungeStaleEntry 方法清除 slotToExpunge 下標(biāo)的過時 Entry,
        // 再從 expungeStaleEntry 方法返回的 null 元素的下標(biāo)開始執(zhí)行 cleanSomeSlots 方法。
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

在方法中 slotToExpunge 變量之所以始終要存儲當(dāng)前區(qū)間段(兩個 null 元素之間)的第一個過時 Entry,是因為每當(dāng)刪除一個過時 Entry 后都會對后續(xù) Entry 進(jìn)行 rehash 操作,如果清理的不是第一個過時 Entry,那么在后續(xù)其他邏輯觸發(fā)清理第一個過時 Entry 時還會將剛剛 rehash 過的元素再次 rehash 一遍,極大的影響效率。

至此在 ThreadLocalMap 中涉及清理過時Entry的三個方法都已剖析完畢,下面我們來羅列一下什么時候會觸發(fā)這三個方法。

清理方法調(diào)用梳理

為避免截圖過多影響閱讀體驗,這里將只粘出調(diào)用的起點,并給調(diào)用鏈路,大家后續(xù)可以自己在源碼中點一點。

get方法

調(diào)用鏈路:ThreadLocal#get->ThreadLocalMap#getEntry->ThreadLocalMap#getEntryAfterMiss->ThreadLocalMap#expungeStaleEntry

圖片圖片

set方法

調(diào)用鏈路 1:ThreadLocal#set->ThreadLocalMap#set->ThreadLocalMap#replaceStaleEntry

調(diào)用鏈路 2:ThreadLocal#set->ThreadLocalMap#set->ThreadLocalMap#cleanSomeSlots

調(diào)用鏈路 3:ThreadLocal#set->ThreadLocalMap#set->ThreadLocalMap#rehash->ThreadLocalMap#expungeStaleEntries->ThreadLocalMap#expungeStaleEntry

圖片圖片

remove方法

調(diào)用鏈路:ThreadLocal#remove->ThreadLocalMap#remove->ThreadLocalMap#expungeStaleEntry

圖片圖片

總結(jié)

通過兩期文章的深度剖析,大家應(yīng)該對 ThreadLocal 的 API 使用以及內(nèi)存泄露問題有了進(jìn)一步的理解。

ThreadLocal 優(yōu)勢是無鎖化提升并發(fā)性能和簡化變量的傳遞邏輯。

在實際業(yè)務(wù)中使用 ThreadLocal 類時應(yīng)該在恰當(dāng)位置調(diào)用 remove 方法顯式移除值。

盡可能的避免觸發(fā) ThreadLocal 清理過時 Entry 的邏輯,從而提高 ThreadLocal 性能。

例如使用繼承的 ThreadLocal 類,并重寫 finalize 方法,確保 ThreadLocal 對象在被垃圾回收前,remove 方法會被調(diào)用。

責(zé)任編輯:武曉燕 來源: Java極客技術(shù)
相關(guān)推薦

2022-08-26 07:33:49

內(nèi)存JVMEntry

2021-04-23 20:59:02

ThreadLocal內(nèi)存

2024-10-28 08:15:32

2018-10-25 15:24:10

ThreadLocal內(nèi)存泄漏Java

2022-10-18 08:38:16

內(nèi)存泄漏線程

2017-01-11 14:02:32

JVM源碼內(nèi)存

2024-06-24 08:11:37

2023-11-03 08:10:49

ThreadLoca內(nèi)存泄露

2023-05-29 07:17:48

內(nèi)存溢出場景

2013-12-23 09:25:21

2021-05-26 08:02:03

ThreadLocal多線程多線程并發(fā)安全

2023-09-22 17:34:37

內(nèi)存remove方法

2021-05-10 11:55:57

ThreadLocal內(nèi)存Java

2024-03-22 13:31:00

線程策略線程池

2010-10-25 10:10:27

ibmdwJava

2010-05-31 16:53:21

Java

2017-01-12 14:52:03

JVMFinalRefere源碼

2011-08-16 09:34:34

Nginx

2020-06-23 09:48:09

Python開發(fā)內(nèi)存

2025-10-15 00:26:20

點贊
收藏

51CTO技術(shù)棧公眾號