ThreadLocal爲什麼會內存泄漏

轉自https://www.jianshu.com/p/a1cd61fa22da

thewindkee個人總結:如果線程使用線程池或者Thread長時間不會消亡,其內部的threadLocalMap也一直存在。而thread.threadLocalMap.set(threadLocal,value)。  這裏threadLocal爲弱引用,(ThreadLocal#ThreadLocalMap#new Entry(threadLocal)產生的弱引用weakRef),value爲強引用。  Entry中弱引用key對應的threadLocal  會在gc的時候 回收,因此value對應的key會變成null.value對應的內存就無法再被訪問,已經泄露了。不過好在threadLocal中 expungeStaleEntry(threadLocal調用get/set/remove觸發) 會清除key爲null的value,一定程度解決了內存泄漏的問題。

ps:當threadLocal 不爲靜態變量,且被回收的時候纔會導致weakRef爲null。

 

ThreadLocal原理回顧

 

ThreadLocal的原理:每個Thread內部維護着一個ThreadLocalMap,它是一個Map。這個映射表的Key是一個弱引用,其實就是ThreadLocal本身,Value是真正存的線程變量Object。

也就是說ThreadLocal本身並不真正存儲線程的變量值,它只是一個工具,用來維護Thread內部的Map,幫助存和取。注意上圖的虛線,它代表一個弱引用類型,而弱引用的生命週期只能存活到下次GC前。

ThreadLocal爲什麼會內存泄漏

ThreadLocal在ThreadLocalMap中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那麼ThreadLocal會在下次JVM垃圾收集時被回收。這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前線程的生命週期很長,一直存在,那麼其內部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鏈的關係一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。

但是JVM團隊已經考慮到這樣的情況,並做了一些措施來保證ThreadLocal儘量不會內存泄漏:在ThreadLocal的get()、set()、remove()方法調用的時候會清除掉線程ThreadLocalMap中所有Entry中Key爲null的Value,並將整個Entry設置爲null,利於下次內存回收。

來看看ThreadLocal的get()方法底層實現

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();
}

在調用map.getEntry(this)時,內部會判斷key是否爲null,繼續看map.getEntry(this)源碼

private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

在getEntry方法中,如果Entry中的key發現是null,會繼續調用getEntryAfterMiss(key, i, e)方法,其內部回做回收必要的設置,繼續看內部源碼:

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

注意k == null這裏,繼續調用了expungeStaleEntry(i)方法,expunge的意思是擦除,刪除的意思,見名知意,在來看expungeStaleEntry方法的內部實現:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot(意思是,刪除value,設置爲null便於下次回收)
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

注意這裏,將當前Entry刪除後,會繼續循環往下檢查是否有key爲null的節點,如果有則一併刪除,防止內存泄漏。

但這樣也並不能保證ThreadLocal不會發生內存泄漏,例如:

  • 使用static的ThreadLocal,延長了ThreadLocal的生命週期,可能導致的內存泄漏。
  • 分配使用了ThreadLocal又不再調用get()、set()、remove()方法,那麼就會導致內存泄漏。

爲什麼使用弱引用?

從表面上看,發生內存泄漏,是因爲Key使用了弱引用類型。但其實是因爲整個Entry的key爲null後,沒有主動清除value導致。很多文章大多分析ThreadLocal使用了弱引用會導致內存泄漏,但爲什麼使用弱引用而不是強引用?

官方文檔的說法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
爲了處理非常大和生命週期非常長的線程,哈希表使用弱引用作爲 key。

下面我們分兩種情況討論:

  • key 使用強引用:引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry內存泄漏。
  • key 使用弱引用:引用的ThreadLocal的對象被回收了,由於ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
    比較兩種情況,我們可以發現:由於ThreadLocalMap的生命週期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。

因此,ThreadLocal內存泄漏的根源是:由於ThreadLocalMap的生命週期跟Thread一樣長,如果沒有手動刪除對應key的value就會導致內存泄漏,而不是因爲弱引用。

總結

綜合上面的分析,我們可以理解ThreadLocal內存泄漏的前因後果,那麼怎麼避免內存泄漏呢?

  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

在使用線程池的情況下,沒有及時清理ThreadLocal,不僅是內存泄漏的問題,更嚴重的是可能導致業務邏輯出現問題。所以,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理。



作者:Misout
鏈接:https://www.jianshu.com/p/a1cd61fa22da
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章