【學習筆記】ThreadLocal與引用類型相關知識點
- 2020 年 8 月 5 日
- 筆記
- JavaSE/JVM
0 寫在前邊
今天以 「TheadLocal 為什麼會導致內存泄漏」 為題與朋友們討論了一波,引出了一些原理性的內容,本文就這個問題作答,並擴展相關的知識點
1 ThreadLocal 和 ThreadLocalMap 是什麼?
簡單來說,ThreadLocal 是一種操作與線程綁定的共享對象的工具,通過ThreadLocal可以將一些對象保存在線程上,實現同線程不同方法之間的對象共享。
線程的上下文由 ThreadLocalMap 組成,它是 ThreadLocal 的靜態內部類,存儲着線程共享對象。
一般來說,我們無需顯式創建ThreadLocalMap,也無需為裝入ThreadLocalMap 對象設 key 值,因為在 set 方法執行時會創建 ThreadLocalMap,並將當前 ThreadLocal 對象作為 key,待存儲對象作為 value,存儲到 ThreadLocalMap。
值得一提的是,ThreadLocalMap 的 key 與 value 的類型是不同的,key 是弱引用類型的,value 是強引用類型的。
2 Thread、ThreadLocal 與 ThreadLocalMap 之間的關係
Thread 與 ThreadLocalMap
首先 ThreadLocalMap 是與 Thread 進行綁定的,ThreadLocalMap 是線程上實際存儲共享對象的容器。
如下圖,threadLocals
就是默認的 ThreadLocalMap,默認為 null
綁定 ThreadLocalMap 到 Thread 的位置在 ThreadLocal 的 createMap
方法中,threadLocals
引用指向 ThreadLocalMap。(這裡還包含了放置第一個對象的操作)
ThreadLocal 的 getMap
方法取的就是線程的 threadLocals
ThreadLocal與ThreadLocalMap
ThreadLocalMap 是 ThreadLocal 類的靜態內部類,ThreadLocal 是操作 ThreadLocalMap 的工具,還是 ThreadLocalMap 的 key 對象,在 ThreadLocal 作為 key 保存前轉換成弱引用類型。
一般我們通過 ThreadLocal 的 set 方法進行保存對象,在 set
方法內部獲取了當前線程的 ThreadLocalMap,調用 ThreadLocalMap 的 set
方法進行保存對象。
使用 this 關健字將當前使用的 ThreadLocal 對象作為 key 存到 ThreadLocalMap 中,以減小 key 衝突的可能性。
ThreadLocalMap 中的 set 方法主要是創建一個 Entry 對象放進數組中,Entry 繼承 WeakReference 類,將 Entry 的 key(也就是 ThreadLocal)轉成弱類型。
一句話總結它們之間的關係
每個 Thread 綁定 ThreadLocalMap 來存儲線程上下文共享對象,ThreadLocalMap 中的key(即,ThreadLocal)在同一線程中是唯一的。單線程情況下,每個 ThreadLocal 只對應一個值對象。
3 ThreadLocal導致的內存泄漏的原因是什麼?
導致內存泄漏的原因在於程序員未在使用完ThreadLocalMap中存儲的對象後清除這些對象。
ThreadLocalMap是維護在Thread內部的,意味着只要線程不退出,ThreadLocalMap中保存的對象引用就會一直存在,由於垃圾回收器是依據可達性分析的,存在強引用的對象不會被回收,而ThreadLocalMap中存儲的對象都是強引用的。
假設當前線程處於一個死循環中(比如,Tomcat),隨着ThreadLocalMap保存的對象越來越多,垃圾收集器無法回收強引用的對象,就會導致可用堆內存越來越小,出現內存泄漏,最終拋出OOM。
4 如何清理 ThreadLocalMap 存儲的對象?
用完 ThreadLocal 存儲的對象後,只需調用 ThreadLocal 的 remove 方法,就會自動將 ThreadLocalMap 中的 K-V 對引用置空,垃圾收集器會在合適的時機內清除 K-V 對象釋放內存。
ThreadLocal 類 remove 方法,獲取當前線程上的 ThreadLocalMap 移除以此 ThreadLocal 為 key 的對象。通過調用 ThreadLocalMap 的 remove 方法實現。
ThreadLocalMap 的 remove 方法中,e.clear()
調用的是key對象繼承的 Reference 類的 clear()
,對 key 引用置空,expungeStaleEntry(i)
對 value 引用置空。
ThreadLocalMap 的 expungeStaleEntry
方法,分別取出 ThreadLocalMap 中的 Entry 的 value 與 Entry 本身先後置空。
5 為什麼ThreadLocalMap使用弱引用key?
ThreadLocalMap 是與線程綁定的,線程不退出,強引用的key對象就不會被垃圾回收,當用戶妥善處理的無用K-V對象就會導致內存泄漏。利用弱引用可以及時被 GC 的特性,回收絕大多數key(除 static 域的全局 key 外),以減緩內存泄漏。
實際上最需要回收的是value對象,弱引用key只是一種挽救措施。
6 ThreadLocalMap 為什麼使用強引用 value,而不是弱引用?
與 key 不同的是,key 僅作為索引,實際工作的是 value,value 需要共享。
當局部 value 對象所在的方法結束,棧楨被清空時,會將局部 value 對象引用銷毀,垃圾收集器會清除沒有引用的對象。
如果此時設置成弱引用裝入 Map,value 對象會在某次 GC 時消亡,這肯定不是我們希望的。
我們希望的是value對象可以維持存活以共享,只有強引用可以達到目的。
7 線程池會累積 ThreadLocalMap 的佔用的內存而出現內存泄漏嗎?
解釋下問題,之前有講過,ThreadLocalMap 與 Thread 的生命周期是一致的,而線程池技術是復用線程的,如果之前的 ThreadLocalMap 已經開始內存泄漏,是否會出現累積已泄漏的內存?
線程池不存在這個問題,雖然它復用了線程,但是清除了上一線程的所有資源。
8 線程有一個ThreadLocalMap,ThreadLocal也只有一個值,為何還會內存泄漏?
這是我自己思考時提出來的,能問出這個問題,只能說當時還沒完全理解ThreadLocal與ThreadLocalMap的對應關係。
原問題:一個線程有一個ThreadLocalMap(不考慮繼承ThreadLocal的那個實現),即然 ThreadLocal 作為 key 了,那麼ThreadLocalMap中是否只會有一個Entry,內存再泄露能泄露到哪裡去?(誤認為ThreadLocalMap與ThreadLocal綁定,只有一個,也只能裝一個Entry,這是錯誤的)
其實 ThreadLocal 我們可以創建很多個,ThreadLocalMap卻只有一個(不考慮繼承ThreadLocal的那個實現),通過創建多個 ThreadLocal 來存取 ThreadLocalMap 中的對象。
偽代碼舉例:
ThreadLocal<A> aThreadLocal = new ThreadLocal<A>();
ThreadLocal<B> bThreadLocal = new ThreadLocal<B>();
aThreadLocal.set(new A("a"));
bThreadLocal.set(new B("b"));
aThreadLocal.get();
bThreadLocal.get();
我在ThreadLocal的getMap()打了斷點,當前線程中 ThreadLocalMap 中有兩個對象,可以看到referent中記錄了保存對象的ThreadLocal對象的HashCode。這起碼證明了ThreadLocalMap不僅僅能裝一個對象
9 【擴展】Java對象的引用類型
- 強引用:常見new的對象,只要還有強引用的對象,則不會被GC
- 軟引用:比強引用弱,僅當JVM內存不足時才會清理,清理時機在OOM前
- 弱引用:只提供非強制的映射關係,會被JVM擇機清理
- 虛引用(幻象引用):無法通過它訪問對象,只確保對象在finalize後執行某些操作
轉載請註明出處:【Hellxz】 //www.cnblogs.com/hellxz/p/java-threadlocal.html