天天看點

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

前幾篇:

<a href="http://blog.csdn.net/xlgen157387/article/details/78114278">Java多線程程式設計-(3)-線程本地ThreadLocal的介紹與使用</a>

<a href="http://blog.csdn.net/xlgen157387/article/details/78297568">Java多線程程式設計-(8)-多圖深入分析ThreadLocal原理</a>

<a href="http://blog.csdn.net/xlgen157387/article/details/78298840">Java多線程程式設計-(9)-ThreadLocal造成OOM記憶體溢出案例示範與原理分析</a>

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

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

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

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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、各引用類型的生命周期及作用:

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

1、示範案例簡介:

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

2、示範代碼:

2、正常執行:

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

3、非正常執行:

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

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

執行結果:

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

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

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

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

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

設定VM參數:

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

最後的運作結果:

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

調試中檢視threadLocal的資料,如下:

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference
Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

可見,雖然這裡我們自己定義了30個ThreadLocal變量,但是最後的确隻有14個,其中還有三個是屬于其他的,還有一點值得注意的是,我們的<code>threadLocal1</code>和<code>threadLocal2</code> 變量,在進行GC垃圾回收的時候,弱引用的Key是沒有進行回收的,最後存活了下來!使得我們最後通過get方法可以擷取到正确的資料。

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

這裡我們就需要重新認識一下,什麼是:當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象,這裡的重點是:隻被弱引用關聯的對象

首先舉個執行個體:

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

由上圖可知道,<code>user</code>和<code>new User()</code>是在不同的記憶體空間的,他們之間是通過引用進行關聯起來的。

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

執行結果如下:

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

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

7、總結

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

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

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

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

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

先看一個簡單的案例:

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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

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

Java多線程程式設計-(18)-借ThreadLocal出現OOM記憶體溢出問題再談弱引用WeakReference

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