《提高能力,漲薪可待》-ThreadLocal的內存泄露的緣由分析以及如何避免

ThreadLocal內存泄露分析

歡迎關注公衆號【Ccww筆記】,原創技術文章第一時間推出java

往期文章:面試

前言

在分析ThreadLocal致使的內存泄露前,須要普及瞭解一下內存泄露、強引用與弱引用以及GC回收機制,這樣才能更好的分析爲何ThreadLocal會致使內存泄露呢?更重要的是知道該如何避免這樣狀況發生,加強系統的健壯性。數組

內存泄露

內存泄露爲程序在申請內存後,沒法釋放已申請的內存空間,一次內存泄露危害能夠忽略,但內存泄露堆積後果很嚴重,不管多少內存,早晚會被佔光,緩存

廣義並通俗的說,就是:再也不會被使用的對象或者變量佔用的內存不能被回收,就是內存泄露。多線程

強引用與弱引用

強引用,使用最廣泛的引用,一個對象具備強引用,不會被垃圾回收器回收。當內存空間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不回收這種對象。併發

若是想取消強引用和某個對象之間的關聯,能夠顯式地將引用賦值爲null,這樣可使JVM在合適的時間就會回收該對象。post

弱引用,JVM進行垃圾回收時,不管內存是否充足,都會回收被弱引用關聯的對象。在java中,用java.lang.ref.WeakReference類來表示。能夠在緩存中使用弱引用。this

GC回收機制-如何找到須要回收的對象

JVM如何找到須要回收的對象,方式有兩種:spa

  • 引用計數法:每一個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時能夠回收,
  • 可達性分析法:從 GC Roots 開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的,那麼虛擬機就判斷是可回收對象。

引用計數法,可能會出現A 引用了 B,B 又引用了 A,這時候就算他們都再也不使用了,但由於相互引用 計數器=1 永遠沒法被回收。線程

ThreadLocal的內存泄露分析

先從前言的瞭解了一些概念(已懂忽略),接下來咱們開始正式的來理解ThreadLocal致使的內存泄露的解析。

實現原理

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ...
   }
複製代碼

ThreadLocal的實現原理,每個Thread維護一個ThreadLocalMap,key爲使用弱引用的ThreadLocal實例,value爲線程變量的副本。這些對象之間的引用關係以下,

實心箭頭表示強引用,空心箭頭表示弱引用

ThreadLocal 內存泄漏的緣由

從上圖中能夠看出,hreadLocalMap使用ThreadLocal的弱引用做爲key,若是一個ThreadLocal不存在外部強引用時,Key(ThreadLocal)勢必會被GC回收,這樣就會致使ThreadLocalMap中key爲null, 而value還存在着強引用,只有thead線程退出之後,value的強引用鏈條纔會斷掉。

但若是當前線程再遲遲不結束的話,這些key爲null的Entry的value就會一直存在一條強引用鏈:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永遠沒法回收,形成內存泄漏。

那爲何使用弱引用而不是強引用??

咱們看看Key使用的

key 使用強引用

當hreadLocalMap的key爲強引用回收ThreadLocal時,由於ThreadLocalMap還持有ThreadLocal的強引用,若是沒有手動刪除,ThreadLocal不會被回收,致使Entry內存泄漏。

key 使用弱引用

當ThreadLocalMap的key爲弱引用回收ThreadLocal時,因爲ThreadLocalMap持有ThreadLocal的弱引用,即便沒有手動刪除,ThreadLocal也會被回收。當key爲null,在下一次ThreadLocalMap調用set(),get(),remove()方法的時候會被清除value值。

ThreadLocalMap的remove()分析

在這裏只分析remove()方式,其餘的方法能夠查看源碼進行分析:

private void remove(ThreadLocal<?> key) {
    //使用hash方式,計算當前ThreadLocal變量所在table數組位置
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    //再次循環判斷是否在爲ThreadLocal變量所在table數組位置
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //調用WeakReference的clear方法清除對ThreadLocal的弱引用
            e.clear();
            //清理key爲null的元素
            expungeStaleEntry(i);
            return;
        }
    }
}
複製代碼

再看看清理key爲null的元素expungeStaleEntry(i):

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

    // 根據強引用的取消強引用關聯規則,將value顯式地設置成null,去除引用
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 從新hash,並對table中key爲null進行處理
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //對table中key爲null進行處理,將value設置爲null,清除value的引用
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}
複製代碼

總結

因爲Thread中包含變量ThreadLocalMap,所以ThreadLocalMap與Thread的生命週期是同樣長,若是都沒有手動刪除對應key,都會致使內存泄漏。

可是使用弱引用能夠多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set(),get(),remove()的時候會被清除。

所以,ThreadLocal內存泄漏的根源是:因爲ThreadLocalMap的生命週期跟Thread同樣長,若是沒有手動刪除對應key就會致使內存泄漏,而不是由於弱引用。

ThreadLocal正確的使用方法

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

  • 將ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強引用,也就能保證任什麼時候候都能經過ThreadLocal的弱引用訪問到Entry的value值,進而清除掉 。

各位看官還能夠嗎?喜歡的話,動動手指點個💗,點個關注唄!!謝謝支持!

歡迎關注公衆號【Ccww筆記】,原創技術文章第一時間推出

相關文章
相關標籤/搜索