從源碼淺析ThreadLocal可能的內存泄漏

從源碼淺析ThreadLocal可能的內存泄漏

ThreadLocal的set方法
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;
}

每個Thread對象有個名爲threadLocals的Map,key是ThreadLocal對象,值是要存儲的Value。這個名爲threadLocals的Map的實現是ThreadLocalMap,它是ThreadLocal的內部類。這樣子,當使用完ThradLocal之後,如果線程不去remove就會引發內存泄露問題。

在這裏插入圖片描述
Map中的Entry的Key是弱引用到ThreadLocal對象,當ThreadLocal外部的強引用置爲Null後,Key會被GC掉,而value還存在着GCroot可達,鏈路如下:Thread->Thread.threadlocalMaps->entry->valueRef。所以value值無法被回收,存在內存泄露問題。

源碼中已有的防內存泄露的改進

ThreadLocalMap中的set方法

private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
}

如果當前table[i]!=null的話說明hash衝突就需要向後環形查找,若在查找過程中遇到髒entry就通過replaceStaleEntry進行處理;

如果當前table[i]==null的話說明新的entry可以直接插入,但是插入後會調用cleanSomeSlots方法檢測並清除髒entry;

插入值之後,再看看cleanSomeSlots方法,會去清理內存泄露的entry

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);
    return removed;
}

找到i之後,往後遍歷,找出key爲null的entry,並且清理掉。n爲了控制掃描範圍的,一開始n是entry的個數,如果發現了內存泄露的entry,則n變爲哈希數組的長度,相當於增大了掃描範圍。nextIndex是環形遞增i。

再看看是如何清理的內存泄露的entry

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

            // expunge entry at staleSlot
            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;
        }

這個方法將tab[staleSlot]及value置爲null,使其可以被回收。接着,這個方法會繼續從staleSlot開始進行環形遍歷,直至遇到第一個下標i對應的entry是null則返回。當是非null時:如果key是null,說明是內存泄露的entry,則進行置null,使其可以被內存回收;如果是哈希桶位置不對,則進行糾正。

爲什麼要使用弱引用?

如果是強引用,如果沒有進行remove則肯定會內存泄露,而弱引用,用於ThreadLocal進行了清理,降低了內存泄露的風險。

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