深入JDK源碼系列--ThreadLocal內存泄漏問題

文章開始先解釋一下內存泄漏和內存溢出,內存泄漏是由於不當操作(不當代碼)使得某些內存無法被操作(回收),導致JVM可使用的內存莫名減少,大量的內存泄漏就會導致內存溢出。內存溢出:我們所需要的內存大於JVM所擁有的內存。言歸正傳,今天主要是來填坑的,上邊文章講了ThreadLocal部分源碼,但是漏了一個remove()和不當操作ThreadLocal導致內存泄漏沒有講。
線程中有個ThreadLocalMap對象,這個對象就是來保存本地變量的,其key就是ThreadLocal對象而value就是其對應的值的。重點我們來看一下這個ThreadLocalMap類,這個類是ThreadLocal的內部類,其數據結構是個Entry對象,重點看這個Entry對象,它的key就是是ThreadLocal對象,而這個key採用的是弱引用,它的value就是ThreadLocal對應的值,而這個value是個強引用,JVM是不會回收value對象的,除非線程結束,對應的線程被銷燬回收,value纔會被回收。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //由於Entry中key是弱引用,當threadLoad對象爲null,
    //由於採用了弱引用,
    //線程雖然key只向了threadLocal對象,
    //但是內存不夠時依舊會回收ThreadLocal對象,
    //由於ThreadLocal對象被回收所以是的Value訪問不了導致了內存泄漏。
    //內存泄漏:由於某些內存訪問不了,導致JVM能使用的內存減少了
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

首先我們來假設一種情況,當我們在線程中使用了一個ThreadLocal對象,當我們使用完後,將這個ThreadLocal對象的引用置爲null,此時我們堆內存中的ThreadLocal對象只有線程中的ThreadLocalMap中的一個弱引用指向它,當JVM進行垃圾回收時會將這些弱引用回收,由於我們的value是強引用是不會被回收的,此時我們無法就訪問這些被回收的ThreadLocal對應的value了,而JVM也回收不了這些value,此時就造成了內存泄漏了。空口無憑,我們來模擬一下。
模擬代碼如下:

public class SimulateMemoryLeak {
    private static final int TASK_SIZE=4;
    static final ThreadPoolExecutor THREAD_POOL=new ThreadPoolExecutor(TASK_SIZE,TASK_SIZE,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10));
    static class ThreadLocalOOM{
       private byte[] bytes=new byte[1024*1024*5];
    }
    ThreadLocal<ThreadLocalOOM> threadLocal;
    private static final CountDownLatch COUNT_DOWN_LATCH=new CountDownLatch(TASK_SIZE);
    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<TASK_SIZE;i++){
            THREAD_POOL.execute(()->{
                COUNT_DOWN_LATCH.countDown();
                try {
                    COUNT_DOWN_LATCH.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                SimulateMemoryLeak simulateMemoryLeak = new SimulateMemoryLeak();
                simulateMemoryLeak.threadLocal=new ThreadLocal<>();
                simulateMemoryLeak.threadLocal.set(new ThreadLocalOOM());
                simulateMemoryLeak.threadLocal=null;
            });
        }
        System.out.println("=========>end");
    }

}

我們採用四個線程去模擬,假設裏面存在一個大對象,這個大對象5M左右,我們將這個大對象放入線程中的ThreadLocalMap當中,並且我們沒有其他引用指向ThreadLocal對象,我們採用JVisualVM工具監測一下內存變化。具體如圖所示:
在這裏插入圖片描述
如圖顯示,我們確實是造成了內存泄漏,看這些鋸齒狀表示發生了垃圾回收,但是已經不能把這些大對象回收掉。如果此時我們再創建一些大對象就會造成OOM了。我們要避免這個問題就得采用ThreadLocal中的remove()方法了,其作用就是將用完的ThreadLocal對應的Entry對象移除ThreadLocalMap中。我們將修改後的代碼再進行監控看一下
在這裏插入圖片描述
結果很明顯,解決了內存泄漏問題。大致講一下remove()主要原理吧:首先它會從線程獲得ThreadLocalMap對象,然後從ThreadLocalMap(其實底層是一個Entry數組)找到ThreadLocal對應的Entry對象,具體是通過hash算法計算出ThreadLocal對象對應的hash值,這個值和ThreadLocalMap 中的數組長度-1相與就是可能的下標誌,由於可能存在hase衝突,所以不一定是,所以需要通過for循環向後找,直到找到對應的Entry對象,然後將Entry對應置空並且移出數組。當JVM進行垃圾回收時就會將原來的Entry對象在堆內存開闢的空間回收調。
核心代碼見java.lang.ThreadLocal.ThreadLocalMap中的remove()方法。

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    //計算下標
    int i = key.threadLocalHashCode & (len-1);
    //由於可能存在hash衝突,所以需要向後尋找
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //將引用置爲空
            e.clear();
            //將Entry移出數組
            expungeStaleEntry(i);
            return;
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章