閱讀781 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Java多線程編程-(18)-借ThreadLocal出現OOM內存溢出問題再談弱引用WeakReference

前幾篇:

Java多線程編程-(3)-線程本地ThreadLocal的介紹與使用

Java多線程編程-(8)-多圖深入分析ThreadLocal原理

Java多線程編程-(9)-ThreadLocal造成OOM內存溢出案例演示與原理分析

一、簡單回顧

在上幾篇的時候,已經簡單的介紹了不正當的使用ThreadLocal造成OOM的原因,以及ThreadLocal的基本原理,下邊我們首先回顧一下ThreadLocal的原理圖以及各類之間的關係:

1、Thread、ThreadLocal、ThreadLocalMap、Entry之間的關係(圖A):

這裏寫圖片描述

上圖中描述了:一個Thread中隻有一個ThreadLocalMap,一個ThreadLocalMap中可以有多個ThreadLocal對象,其中一個ThreadLocal對象對應一個ThreadLocalMap中一個的Entry實體(也就是說:一個Thread可以依附有多個ThreadLocal對象)。

2、ThreadLocal各類引用關係(圖B):

在ThreadLocal的生命周期中,都存在這些引用。( 實線代表強引用,虛線代表弱引用)

這裏寫圖片描述

ThreadLocal到Entry對象key的引用斷裂,而不及時的清理Entry對象,可能會造成OOM內存溢出!

二、引用的類型

我們對引用的理解也許很簡單,就是:如果 reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱這塊內存代表著一個引用。但是書上說的這種方式過於狹隘,一個對象在這種定義下隻有被引用或者沒有被引用兩種狀態,對於如何描述一些“食之無味,棄之可惜”的對象就顯得無能為力。我們希望能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;如果內存在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多係統的緩存功能都符合這樣的應用場景。

一般的引用類型分為:強引用( Strong Reference)、軟引用( Soft Reference)、弱引用( Weak Reference)、虛引用( Phantom Reference)四種,這四種引用強度依次逐漸減弱。

1、下邊是四中類型的介紹:

(1)就是指在程序代碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,隻要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象,也就是說即使Java虛擬機內存空間不足時,GC收集器也絕不會回收該對象,如果內存空間不夠就會導致內存溢出。

(2)用來描述一些還有用,但並非必需的對象。對於軟引用關聯著的對象,在係統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中並進行回收,以免出現內存溢出。如果這次回收還是沒有足夠的內存,才會拋出內存溢出異常。在 JDK 1.2 之後,提供了 SoftReference 類來實現軟引用。

軟引用適合引用那些可以通過其他方式恢複的對象,例如:數據庫緩存中的對象就可以從數據庫中恢複,所以軟引用可以用來實現緩存。等會會介紹MyBatis中的使用軟引用實現緩存的案例。

(3)也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉隻被弱引用關聯的對象。在 JDK 1.2 之後,提供了 WeakReference 類來實現弱引用。ThreadLocal使用到的就有弱引用。

(4)也稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是希望能在這個對象被收集器回收時收到一個係統通知。在 JDK 1.2 之後,提供了PhantomReference 類來實現虛引用。

2、各引用類型的生命周期及作用:

這裏寫圖片描述

三、ThreadLocal中的弱引用

上述我們知道了當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉隻被弱引用關聯的對象。我們的ThreadLocal中ThreadLocalMap中的Entry類的key就是弱引用的,如下:

這裏寫圖片描述

而弱引用會在垃圾收集器工作的時候進行回收,也就是說,隻要執行垃圾回收,這些對象就會被回收,也就是上述圖B中的虛線連接的地方斷開了,就成了一個沒有key的Entry,下邊演示一下:

1、演示案例簡介:

我們知道一個線程Thread可以有多個ThreadLocal變量,這些變量存放在Thread中的ThreadLocalMap變量中,那麼我們下邊就在主線程main中定義多個ThreadLocal變量,然後我們想辦法執行幾次GC垃圾回收,再看一下ThreadLocalMap中Entry數組的變化情況。

2、演示代碼:

public class ThreadLocalWeakReferenceGCDemo {

    private static final int THREAD_LOOP_SIZE = 20;

    public static void main(String[] args) throws InterruptedException {

        try {
            //等待連接JConsole
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 1; i < THREAD_LOOP_SIZE; i++) {
            ThreadLocal<Map<Integer, String>> threadLocal = new ThreadLocal<>();
            Map<Integer, String> map = new HashMap<>();
            map.put(i, "我是第" + i + "個ThreadLocal數據!");
            threadLocal.set(map);
            threadLocal.get();

            System.out.println("第" + i + "次獲取ThreadLocal中的數據");

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

2、正常執行:

當for循環執行到最後一個的時候,看一下ThreadLocalMap的情況:

這裏寫圖片描述

可以看到此時的ThreadLocalMap中有21個ThreadLocal變量(也就是21個Entry),其中有3個表示main線程中表示的其他ThreadLocal變量,這是正常的執行,並沒有發生GC收集。

3、非正常執行:

當for循環執行到中間的時候手動執行GC收集,然後再看一下:

通過JConsole工具手動執行GC收集:

這裏寫圖片描述

執行結果:

這裏寫圖片描述

可以看出算上主線程中其他的Entry一共還有6個,也就可以證明在執行GC收集的時候,弱引用被回收了。

4、你可能會問道,弱引用被回收了隻是回收了Entry的key引用,但是Entry應該還是存在的吧?

事情是這樣的,我們的ThreadLocal已經幫我們把key為null的Entry清理了,在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap裏所有key為null的value。

這裏寫圖片描述

上述源碼中描述了清除並重建索引的過程,源碼過多,不截圖顯示。所以,我們最後看到的實際上是已經清除過key為null的Entry之後的結果。這也說明了正常情況下使用ThreadLocal是不會出現OOM內存溢出的,出現內存溢出是和弱引用沒有半點關係的!

5、上述代碼雖然是手動執行的GC,但正常情況下的GC也是會回收弱引用的

如下(注意:實驗請適當調節參數,避免電腦死機),假如我們上述的代碼的主函數main改成如下方式:

public static void main(String[] args) throws InterruptedException {

        ThreadLocal<Map<Integer, String>> threadLocal1 = new ThreadLocal<>();
        Map<Integer, String> map1 = new HashMap<>(1);
        map1.put(1, "我是第1個ThreadLocal數據!");
        threadLocal1.set(map1);

        ThreadLocal<Map<Integer, String>> threadLocal2 = new ThreadLocal<>();
        Map<Integer, String> map2 = new HashMap<>(1);
        map2.put(2, "我是第2個ThreadLocal數據!");
        threadLocal2.set(map2);

        for (int i = 3; i <= MAIN_THREAD_LOOP_SIZE; i++) {
            ThreadLocal<Map<Integer, String>> threadLocal = new ThreadLocal<>();
            Map<Integer, String> map = new HashMap<>(1);
            map.put(i, "我是第" + i + "個ThreadLocal數據!");
            threadLocal.set(map);
            threadLocal.get();

            if (i > 20) {
                //-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8
                //會觸發GC
                byte[] allocation1, allocation2, allocation3, allocation4;
                allocation1 = new byte[2  1024  1024];
                allocation2 = new byte[2  1024  1024];
                allocation3 = new byte[2  1024  1024];
                allocation4 = new byte[4  1024  1024];
            }
        }
        System.out.println("-------" + threadLocal1.get());
        System.out.println("-------" + threadLocal2.get());
    }

設置VM參數:

這裏寫圖片描述

最後的運行結果:

這裏寫圖片描述

調試中查看threadLocal的數據,如下:

這裏寫圖片描述

這裏寫圖片描述

可見,雖然這裏我們自己定義了30個ThreadLocal變量,但是最後的確隻有14個,其中還有三個是屬於其他的,還有一點值得注意的是,我們的threadLocal1threadLocal2 變量,在進行GC垃圾回收的時候,弱引用的Key是沒有進行回收的,最後存活了下來!使得我們最後通過get方法可以獲取到正確的數據。

6、為什麼threadLocal1和threadLocal2變量沒有被回收?

這裏我們就需要重新認識一下,什麼是:,這裏的重點是:

首先舉個實例:

public class WeakRefrenceDemo {

    public static void main(String[] args) {
        User user = new User("hello", "123");
        WeakReference<User> userWeakReference = new WeakReference<>(user);
        System.out.println(userWeakReference.get());
        //另一種方式觸發GC,強製執行GC
        System.gc();
        System.runFinalization();
        System.out.println(userWeakReference.get());
    }

    public static class User {
        private String userName;
        private String userPwd;
        //省去全參構造方法和toString()方法
    }
}

執行結果:

這裏寫圖片描述

可以看到,上述過程盡管GC執行了垃圾收集,但是弱引用還是可以訪問到結果的,也就是沒有被回收,這是因為除了一個弱引用userWeakReference 指向了User實例對象,還有user指向User的實例對象,隻有當user和User實例對象的引用斷了的時候,弱引用的對象才會被真正的回收,看下圖:

這裏寫圖片描述

由上圖可知道,usernew User()是在不同的內存空間的,他們之間是通過引用進行關聯起來的。

如果把上述主函數改成代碼如下,將user = null,則斷開了他們之間的引用關係,但是還有一個弱引用userWeakReference 指向new User()

public static void main(String[] args) {

        User user = new User("hello", "123");
        WeakReference<User> userWeakReference = new WeakReference<>(user);
        System.out.println(userWeakReference.get());
        user = null; //斷開引用
        System.gc(); //強製執行GC
        System.runFinalization();
        System.out.println(userWeakReference.get());
    }

執行結果如下:

這裏寫圖片描述

可以看到斷開了user和new User()之間的引用之後,就隻有弱引用了,因此,上述的那段話:都會回收掉隻被弱引用關聯的對象。因此該new User()會被回收。

因此,就出現了最開始看到的threadLocal1、threadLocal2都還可以訪問到數據(for循環裏邊的,由於作用於的問題,引用已經斷開了),那我我們隻有通過手動設為null的方式,看一下效果,代碼改為如下:

    public static void main(String[] args) throws InterruptedException {

        ThreadLocal<Map<Integer, String>> threadLocal1 = new ThreadLocal<>();
        Map<Integer, String> map1 = new HashMap<>(1);
        map1.put(1, "我是第1個ThreadLocal數據!");
        threadLocal1.set(map1);
        threadLocal1 = null;
        System.gc(); //強製執行GC
        System.runFinalization();

        ThreadLocal<Map<Integer, String>> threadLocal2 = new ThreadLocal<>();
        Map<Integer, String> map2 = new HashMap<>(1);
        map2.put(2, "我是第2個ThreadLocal數據!");
        threadLocal2.set(map2);
        threadLocal2 = null;
        System.gc(); //強製執行GC
        System.runFinalization();

        ThreadLocal<Map<Integer, String>> threadLocal3 = new ThreadLocal<>();
        Map<Integer, String> map3 = new HashMap<>(1);
        map3.put(3, "我是第3個ThreadLocal數據!");
        threadLocal3.set(map3);

        ThreadLocal<Map<Integer, String>> threadLocal4 = new ThreadLocal<>();
        Map<Integer, String> map4 = new HashMap<>(1);
        map4.put(4, "我是第4個ThreadLocal數據!");
        threadLocal4.set(map4);
        System.out.println("-------" + threadLocal3.get());
    }
}

執行結果:

這裏寫圖片描述

可以看到,是我們想要的結果,弱引用也被回收了。

另外還有一種可能是,我們得到的結果有3個,分別是2、3、4,這是有可能的,這是由於垃圾回收器是一個優先級較低的線程, 因此不一定會很快發現那些隻具有弱引用的對象,即隻有等到係統垃圾回收機製運行時才會被回收。但是我們已經看到了我們想要的結果。

7、總結

到了這裏,你應該明白,並不是所有弱引用的對象都會在第二次GC回收的時候被回收,而是回收掉隻被弱引用關聯的對象。因此,使用弱引用的時候要注意到!希望以後在麵試的時候,不要上來張口就說,弱引用在第二次執行GC之後就會被回收!知其然,知其所以然!

四、引用隊列

在很多場景中,我們的程序需要在一個對象的可達性(GC可達性,判斷對象是否需要回收)發生變化的時候得到通知,引用隊列就是用於收集這些信息的隊列。

在創建SoftReference對象時,可以為其關聯一個引用隊列,當SoftReference所引用的對象被回收的時候,Java虛擬機就會將該SoftReference對象添加到預支關聯的引用隊列中。

需要檢查這些通知信息時,就可以從引用隊列中獲取這些SoftReference對象。

不僅僅是SoftReference支持使用引用隊列,軟引用和虛引用也可以關相應的引用隊列。

這裏寫圖片描述

先看一個簡單的案例:

public class WeakCache {

    private void printReferenceQueue(ReferenceQueue<Object> referenceQueue) {
        WeakEntry sv;
        while ((sv = (WeakEntry) referenceQueue.poll()) != null) {
            System.out.println("引用隊列中元素的key:" + sv.key);
        }
    }

    private static class WeakEntry extends WeakReference<Object> {
        private Object key;

        WeakEntry(Object key, Object value, ReferenceQueue<Object> referenceQueue) {
            //調用父類的構造函數,並傳入需要進行關聯的引用隊列
            super(value, referenceQueue);
            this.key = key;
        }
    }

    public static void main(String[] args) {
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        User user = new User("xuliugen", "123456");
        WeakCache.WeakEntry weakEntry = new WeakCache.WeakEntry("654321", user, referenceQueue);
        System.out.println("還沒被回收之前的數據:" + weakEntry.get());

        user = null;
        System.gc(); //強製執行GC
        System.runFinalization();

        System.out.println("已經被回收之後的數據:" + weakEntry.get());
        new WeakCache().printReferenceQueue(referenceQueue);
    }
}

執行結果:

這裏寫圖片描述

ReferenceQueue引用隊列記錄了GC收集器回收的引用,這樣的話,我們就可以通過引用隊列的數據來判斷引用是否被回收,以及被回收之後做相應的處理,例如:如果使用弱引用做緩存則需要清除緩存,或者重新設置緩存等。

其實,上述的代碼,是從MyBatis的源碼中抽離出來的,MyBatis在緩存的時候也提供了對弱引用和軟引用的支持,MyBatis相關的源碼如下:

這裏寫圖片描述

任何一個牛逼的框架,也是一個一個知識點的使用。這篇文章的內容很多,似乎有點又長又臭,不過還是希望對你有所幫助!另外,個人能力有限,難免有所疏漏,如果有也請你及時提出來,以免誤人子弟。

最後更新:2017-11-13 21:34:05

  上一篇:go  雲計算技術的躍進睿雲智合專業先進水平
  下一篇:go  Keras新手“入坑”指南