天天看點

ThreadLocal 記憶體洩露分析

記憶體洩漏(memory leak ):是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩漏似乎不會有大的影響,但記憶體洩漏堆積後的後果就是記憶體溢出。 

記憶體溢出(Out Of Memory,簡稱OOM)是指應用系統中存在無法回收的​​記憶體​​​或使用的​​記憶體​​​過多,最終使得程式運作要用到的​​記憶體​​​大于能提供的最大記憶體。此時​​程式​​​就運作不了,系統會提示記憶體溢出,有時候會​​自動​​​​關閉​​​軟體,重新開機電腦或者軟體後釋放掉一部分記憶體又可以正常運作該軟體,而由​​系統配置​​​、​​資料流​​、使用者代碼等原因而導緻的記憶體溢出錯誤,即使使用者重新執行任務依然無法避免

        線程Thread對象中,每個線程對象内部都有一個的ThreadLocalMap對象。如果這個對象存儲了多個大對象,則可能早出記憶體溢出OOM。為了防止這種情況發生,在ThreadLocal的源碼中,有對應的政策,即調用 get()、set()、remove() 方法,均會清除 ThreadLocal内部的 記憶體。

         ThreadLocal的内部是ThreadLocalMap。ThreadLocalMap内部是由一個Entry數組組成。Entry類的構造函數為 Entry(弱引用的ThreadLocal對象, Object value對象)。因為Entry的key是一個弱引用的ThreadLocal對象,是以在 垃圾回收 之前,将會清除此Entry對象的key。那麼, ThreadLocalMap 中就會出現 key 為 null 的 Entry,就沒有辦法通路這些 key 為 null 的 Entry 的 value。這些 value 被Entry對象引用,是以value所占記憶體不會被釋放。若在指定的線程任務裡面,調用ThreadLocal對象的get()、set()、remove()方法,可以避免出現記憶體洩露。

         下圖虛線表示弱引用。ThreadLocal對象被GC回收了,那麼key變成了null。Map又是通過key拿到的value的對象。是以,GC在回收了key所占記憶體後,沒法通路到value的值,因為需要通過key才能通路到value對象。如圖所示的引用鍊:CurrentThread → Map(ThreadLocalMap) → Entry → value ,是以在當線程沒有被回收的情況下,value所占記憶體也不會被回收。是以可能會造成了記憶體溢出。

ThreadLocal 記憶體洩露分析

         虛線表示是弱引用。弱引用隻要繼承WeakReference<T>類即可。是以說,當ThreadLocal對象被GC回收了以後,Entry對象的key就變成null了。這個時候沒法通路到 Object Value了。并且最緻命的是,Entry持有Object value。是以,value的記憶體将不會被釋放。

         因為上述的原因,在ThreadLocal這個類的get()、set()、remove()方法,均有實作回收 key 為 null 的 Entry 的 value所占的記憶體。是以,為了防止記憶體洩露(沒法通路到的記憶體),在不會再用ThreadLocal的線程任務末尾,調用一次 上述三個方法的其中一個即可。

         是以,可以了解到為什麼JDK源碼中要把Entry對象,用 弱引用的ThreadLocal對象,設計為key,那是因為要手動編寫代碼釋放ThreadLocalMap中 key為null的Entry對象。

         GC什麼時候回收弱引用的對象?弱引用對象是存活到下一次垃圾回收發生之前對象。

         綜上:JVM就會自動回收某些對象将其置為null,進而避免OutOfMemory的錯誤。弱引用的對象可以被JVM設定為null。我們的代碼通過判斷key是否為null,進而 手動釋放 記憶體洩露的記憶體。

為什麼要将ThreadLocal設計為弱引用?

因為弱引用的對象的生命周期直到下一次垃圾回收之前被回收。弱引用的對象将會被置為null。我們可以通過判斷弱引用對象是否已經為null,來進行相關的操作。在ThreadLocalMap中,如果鍵ThreadLocal已經被回收,說明ThreadLocal對象已經為null,是以其對應的值已經無法被通路到。這個時候,需要及時手動編寫代碼清理掉這個鍵值對,防止記憶體洩露導緻的記憶體溢出。

1 強引用

強引用雖然在開發過程中并不怎麼提及,但是無處不在,例如我們在一個對象中通過如下代碼執行個體化一個StringBuffer對象

StringBuffer buffer = new StringBuffer();      

我們知道StringBuffer的執行個體通常是被建立在堆中的,而目前對象持有該StringBuffer對象的引用,以便後續的通路,這個引用,就是一個強引用。

對GC知識比較熟悉的可以知道,HotSpot JVM目前的垃圾回收算法一般預設是可達性算法,即在每一輪GC的時候,標明一些對象作為GC ROOT,然後以它們為根發散周遊,周遊完成之後,如果一個對象不被任何GC ROOT引用,那麼它就是不可達對象,則在接下來的GC過程中很可能會被回收。

強引用最重要的就是它能夠讓引用變得強(Strong),這就決定了它和垃圾回收器的互動。具體來說,如果一個對象通過一串強引用連結可到達(Strongly reachable),它是不會被回收的。如果你不想讓你正在使用的對象被回收,這就正是你所需要的。

2 軟引用

軟引用是用來描述一些還有用但是并非必須的對象。對于軟引用關聯着的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象列進回收傳回之後進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。JDK1.2之後提供了SoftReference來實作軟引用。

相對于強引用,軟引用在記憶體充足時可能不會被回收,在記憶體不夠時會被回收。

3 弱引用

弱引用也是用來描述非必須的對象的,但它的強度更弱,被弱引用關聯的對象隻能生存到下一次GC發生之前,也就是說下一次GC就會被回收。JDK1.2之後,提供了WeakReference來實作弱引用。

4 虛引用

繼續閱讀