<二>深入理解Threadlocal 關於內存泄漏的思考

不知道經常使用  Threadlocal  的朋友有沒有意識到內存泄漏這一點。

什麼是內存泄漏呢?對象已經沒有在其它地方被使用了,但是垃圾回收器沒辦法移除它們,因爲還在被引用着。

我不用的對象,又不能被垃圾回收,就會造成內存泄漏。不瞭解垃圾回收的朋友看這篇文章:垃圾回收的細節

簡單的拿個圖表示下:


如果你瞭解垃圾回收機制,活着看過周志明老師的 深入理解java虛擬機 第二版, 你肯定 知道

強,軟,弱,虛。四種引用關係。在進行GC時,只有強引用關係存在的對象才不會被垃圾回收。

而 ThreadLocalMapl裏的 enter對象 繼承了  jdk  WeakReference (弱引用API)提供的  的key ,也就是 ThreadLocal 取的是 WeakReference提供的弱引用對象,所以在GC時, ThreadLocal 會被垃圾回收期回收掉, Entry對象的key就爲null了,然後 value 卻是強引用 無法回收。

先上代碼再上圖~

 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;
            }
        }
super ( k );  k 是從 weakReference裏得到的,弱引用無疑,value ,自己的Object ,一般都是強引用。

把它們的 堆棧圖 畫出來,讓大家更好的理解:


這個圖應該闡述得很清楚了~

每個Thread都有自己的 一個 ThreadLocalMap。  key 是 TreadLocal 實例對象, value 就是 你要保存的那個 變量

圖中紅色部分描述的了 ThreadLocalMap 是通過WeakReference 包裝了 TreadLocal ,取的是 TreadLocal的弱引用 對象。

那麼在GC 的時候就會造成 TreadLocal 肯定就會被回收掉。 Entry對象的key就爲null了,然後 value 卻是強引用 無法回收。

如果這個方法又長時間不結束的話,就有會這麼一條 強引用的 GCROOT 引用鏈 的存在

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value ; value 就一直不會被回收, 因爲它的 另一半 key 已經不存在了,所以它也不會被調用。 這就造成了內存泄漏。

但是!這可是大名鼎鼎的JDK誒,1.9都出來了,肯定考慮到這個點了,於是在1.5的時候加入了remove 方法 解決這個問題 ; 

後來又針對:怕部分程序員還是忘記調用remove 方法,又在get方法 中做了優化, 我們看看源碼,是哪裏優化了:

這裏就不貼源碼了, 認真的朋友可以進入自己的 開發工具 跟着下面 的 註釋,一個一個的點進去看,一目瞭然。

源碼的進入姿勢是 ThreadLocal.class 的 get( )  -> ThreadLocalMap 的 getEntry  ( this ) 方法, 

當key 不滿足 判斷條件時, 進入 getEntryAfterMiss(key, i, e)  方法 ;

當key == null  ->  expungeStaleEntry(i);  如果 k == null 時,  e.value = null;

看到這裏,意思就是在Threadlocal 在調用get 方法的時候,如果key 是 null  就會把 value 的強引用關係清除掉。

這樣 Threadlocal  被垃圾回收掉的時候  保存的 副本變量 也會被 垃圾回收 從而避免了 部分次數的 內存泄漏。

但這並不能,完全的避免內存泄漏, 仍然需要我們在 調用set  方法後  顯示的  調用remove()方法。

remove 方法 的源碼 , 其實就是清除了 entry 對象的引用 關係,

然後又調用了 expungeStaleEntry(i) 方法,key ==null時 , e.value =null ;

所以我們應該有意識的形成良好的編程習慣/規範,在使用完ThreadLocal之後,記得調用一下remove方法。從而避免內存泄漏。

到這裏,ThreadLocal 造成內存泄漏的原因以及解決辦法以及分析完了。

上一篇中 <一>深入理解面試常問的Threadlocal的實現原理 提到了 主題內容的第三部分也分析完了。


我們再來進行主題四:思考 和 總結 學習的 這個 ThreadLocal;

先來思考一個問題: 我們知道了內存泄漏是因爲  ThreadLocalMap 中 entry 對象的 key 去的是 ThreadLocal的弱引用對象。

那我是不是將  ThreadLocal 的弱引用 換成 強引用 就不會引起內存泄漏了呢?

於是我們拿 key 取弱引用對象 跟 強引用對象 做個對比,再分析分析優缺點~

key 是 強引用: 

如果我們從 ThreadLocal 裏面 已經取到了我們想要的  線程副本 value ,我們是不是就希望 ThreadLocal能夠被垃圾回收掉呢? 但是因爲 ThreadLocalMap  中的 entry 還持有對  ThreadLocal  的強引用。 所以導致 ThreadLocal 遲遲都不能被垃圾回收。所以value 也不能被垃圾回收,從而造成了 entry 對象 發生內存泄漏。 

key 是 弱引用:

首先我們看key~  ThreadLocal 被垃圾回收時,就算 ThreadLocalMap  持有  ThreadLocal  的引用也沒有關係,這是一種弱引用關係, 即使我們沒有手動的將  ThreadLocal 設置爲 null ,垃圾回收器還是會將 ThreadLocal 回收掉。

再看value~ value 在調用get  remove 方法的時候也會被垃圾回收。

對比分析後,我們可以發現,如果key 是弱引用,我調用 remove 方法 就能避免value 對象的內存泄漏。

                                      如果key  是強引用,我用完了 ThreadLocal 我還得將 ThreadLocal 設置爲null,value也設置爲null

最後發現:哦~造成內存泄漏的根本原因並不是弱引用關係所導致的,真正的原因是:(這裏我們提到一個生命週期的概念)

ThreadLocalMap和Thread的生命週期一樣長,而 ThreadLocal  實際上較短(因爲我用完就不需要它了)。

在沒有手動的刪除key 的情況下,就會造成泄漏, JDK 現在用的弱引用 優化了 在程序員失誤的情況下,我只內存泄漏value,

並且提供了不泄漏value 的 API 方法 :顯示調用 remove方法。而用強引用, 那我key 和 value 全部都可能內存泄漏。

那麼不知道大家是否想起了其它情況下的內存泄漏,比如集合類,數據庫資源那些的。

其根本問題全在這兒:引用方和被引用方的生命週期長短不一致導致的。 這算是對多種情況的一個上層抽象吧~


這麼分析了一波 ThreadLocal 能給我們帶來什麼。

1、瞭解了 ThreadLocal 的實現原理,從而能更好的使用 ThreadLocal ,能避免內存泄漏的情況。

2、能規範我們的編碼習慣,並抽象出了內存泄漏的原因,以後編碼時有意識考慮這些問題。

3、ThreadLocal 能實現每個線程都有一份變量副本,其實就是空間換時間的設計思路,因爲每個線程都有個ThreadLocalMap

從而實現了   另一種意義上的 “無鎖編程”。

4、  你懂的~

最後 ThreadLocal  就跟加鎖後要釋放鎖一樣的, 用完記得調用 remove 方法。

希望大家看完有所收穫,同時 資歷尚淺,還請多多指正。 大笑

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