從源碼角度分析導致 JVM 內存泄露的 ThreadLocal

文章目錄

  • 1. 為什么需要ThreadLocal
  • 2. ThreadLocal的實現解析
    • 1.1 實現分析
    • 1.2 具體實現
    • 1.3 ThreadLocalMap中Hash沖突的解決
      • 1.3.1 Hash沖突解決的幾種方法
        • 1.3.1.1 開放定值法
        • 1.3.1.2 鏈地址法
        • 1.3.1.3再哈希法:
        • 1.3.1.4 建立公共溢出區
      • 1.3.2 ThreadLocal解決Hash沖突的方法
  • 3. ThreadLocal引發的內存泄漏分析
    • 3.1 強引用、軟應用、弱引用和虛引用
    • 3.2 內存泄露的現象
      • 3.2.1 第一組測試
      • 3.2.2 第二組測試
      • 3.2.3 第三組測試
      • 3.2.4 第四組測試
    • 3.3 內存泄露的分析
    • 3.4 總結


1. 為什么需要ThreadLocal

我們從Java操作實現MySQL事務的角度去看

不難發現,如果通過上面的步驟,那么開啟事務的連接和執行操作的連接不是同一個,勢必會導致事務的失效

那么怎么解決上面的問題呢?

一種簡單直接的方法就是,執行操作的時候將開啟事務的連接作為方法參數傳遞給實際操作的方法

但是我們平常在使用Mybatis的時候從來沒有這樣傳遞過參數

也就是說spring自動幫我們解決了這個問題

那么它是怎么做到的?? Spring 是使用一個 ThreadLocal 來實現“綁定連接到線程”的。

ThreadLocal:

此類提供線程局部變量。這些變量與普通對應變量的不同之處在于, 訪問一 個變量的每個線程(通過其 get 或 set 方法)都有自己獨立初始化的變量副本。 ThreadLocal 實例通常是希望將狀態與線程(例如, 用戶 ID 或事務 ID)相關聯 的類中的私有靜態字段。也就是說 ThreadLocal 為每個線程都提供了變量的副本,使得每個線程在某 一時間訪問到的并非同一個對象,這樣就隔離了多個線程對數據的數據共享。

ThreadLocal 的一大應用場景就是跨方法進行參數傳遞,比如 Web 容器中, 每個完整的請求周期會由一個線程來處理。 結合 ThreadLocal 再使用 Spring 里的 IOC 和 AOP,就可以很好的解決我們上面的事務的問題。只要將一個數據庫連接 放入 ThreadLocal 中, 當前線程執行時只要有使用數據庫連接的地方就從ThreadLocal 獲得就行了。

ThreadLocal 類接口很簡單,只有 4 個方法,我們先來了解一下:

? void set(Object value)

設置當前線程的線程局部變量的值。

? public Object get()

該方法返回當前線程所對應的線程局部變量。

? public void remove()

將當前線程局部變量的值刪除, 目的是為了減少內存的占用, 該方法是 JDK 5.0 新增的方法。

? protected Object inialValue()

返回該線程局部變量的初始值,該方法是一個 protected 的方法, 顯然是為 了讓子類覆蓋而設計的。這個方法是一個延遲調用方法, 在線程第 1 次調用 get() 或 set(Object)時才執行,并且僅執行 1次。ThreadLocal 中的缺省實現直接返回一 個 null。

2. ThreadLocal的實現解析

1.1 實現分析

怎么實現 ThreadLocal,既然說讓每個線程都擁有自己變量的副本,最容易 的方式就是用一個Map 將線程的副本存放起來, Map 里 key 就是每個線程的唯 一性標識,比如線程 ID ,value 就是副本值, 實現起來也很簡單:

public class MyThreadLocal<T> {private Map<Thread, T> threadMap = new HashMap<>();public synchronized T get() {return threadMap.get(Thread.currentThread());}public synchronized void set(T t) {threadMap.put(Thread.currentThread(), t);}
}

考慮到并發安全性, 對數據的存取用 synchronize 關鍵字加鎖, 但是 DougLee 在《并發編程實戰》中為我們做過性能測試

可以看到 ThreadLocal 的性能遠超類似 synchronize 的鎖實現 ReentrantLock, 比AtomicInteger 也要快很多,即使我們把 Map 的實現更換為Java 中專為并發設計的 ConcurrentHashMap也不太可能達到這么高的性能。

怎么樣設計可以讓 ThreadLocal 達到這么高的性能呢?最好的辦法則是讓變 量副本跟隨著線程本身, 而不是將變量副本放在一個地方保存, 這樣就可以在存 取時避開線程之間的競爭。

同時,因為每個線程所擁有的變量的副本數是不定的, 有些線程可能有一個, 有些線程可能有 2個甚至更多, 則線程內部存放變量副本需要一個容器, 而且容器要支持快速存取, 所以在每個線程內部都可以持有一個 Map 來支持多個變量副本,這個 Map 被稱為 ThreadLocalMap

1.2 具體實現

上面說到的ThreadLocalMap,實際上就實現了讓變量副本跟隨著線程

可以看到,這個ThreadLocalMap實際上就是一個Map,以ThreadLocal作為鍵,用戶數據作為值

雖然這個類定義在ThreadLocal類中,但是它是聲明在Thread中的

這樣就能讓變量副本跟隨者線程

我們再來看ThreadLocalget方法

面先取到當前線程,然后調用 getMap 方法獲取對應的 ThreadLocalMap,再從ThreadLocalMap中獲取當前ThreadLocal對應的值

從而實現一個線程能保存多份變量副本

1.3 ThreadLocalMap中Hash沖突的解決

1.3.1 Hash沖突解決的幾種方法

1.3.1.1 開放定值法

基本思想是,出現沖突后按照一定算法查找一個空位置存放,根據算法的不 同又可以分為線性探測再散列、二次探測再散列、偽隨機探測再散列。

  • 線性探測再散列,即依次向后查找
  • 二次探測再散列, 即依次向前后查找, 增 量為 1 、2 、3 的二次方
  • 偽隨機,顧名思義就是隨機產生一個增量位移
1.3.1.2 鏈地址法

這種方法的基本思想是將所有哈希地址為 i 的元素構成一個稱為同義詞鏈的單鏈表, 并將單鏈表的頭指針存在哈希表的第 i 個單元中, 因而查找、插入和刪 除主要在同義詞鏈中進行。鏈地址法適用于經常進行插入和刪除的情況。 Java里的 HashMap 用的就是鏈地址法,為了避免 hash 洪水攻擊,1.8 版本開始還引 入了紅黑樹

1.3.1.3再哈希法:

這種方法是同時構造多個不同的哈希函數: Hi=RH1(key) i=1 ,2 ,… ,k 當哈希地址 Hi=RH1(key)發生沖突時,再計算 Hi=RH2(key)……,直到沖突 不再產生。這種方法不易產生聚集,但增加了計算時間

1.3.1.4 建立公共溢出區

這種方法的基本思想是: 將哈希表分為基本表和溢出表兩部分, 凡是和基本 表發生沖突的元

素, 一律填入溢出表。

1.3.2 ThreadLocal解決Hash沖突的方法

ThreadLocal 里用的是線性探測再散列

3. ThreadLocal引發的內存泄漏分析

3.1 強引用、軟應用、弱引用和虛引用

  • 強引用就是指在程序代碼之中普遍存在的, 類似“Object obj=new Object()” 這類的引用, 只要強引用還存在, 垃圾收集器永遠不會回收掉被引用的對象實例。
  • 軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關聯著的對象, 在系統將要發生內存溢出異常之前, 將會把這些對象實例列進回收范圍之中進行 第二次回收。如果這次回收還沒有足夠的內存, 才會拋出內存溢出異常。在 JDK 1.2 之后,提供了 SoReference 類來實現軟引用。
  • 弱引用也是用來描述非必需對象的, 但是它的強度比軟引用更弱一些,被弱引用關聯的對象實例只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象實例。在 JDK 1.2 之 后,提供了WeakReference 類來實現弱引用。
  • 虛引用也稱為幽靈引用或者幻影引用, 它是最弱的一種引用關系。一個對象實例是否有虛引用的存在, 完全不會對其生存時間構成影響, 也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象實例被收集器回收時收到一個系統通知。在 JDK 1.2 之后,提供了PhantomReference 類來實現虛引用。

3.2 內存泄露的現象


public class ThreadLocalMemoryLeakTest {// 創建固定大小的線程池,核心線程和最大線程數都是5final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingDeque<>());// 定義一個占用5M內存的本地變量類static class LocalVariable {// 大約5M的字節數組byte[] bytes = new byte[1024 * 1024 * 5];}// ThreadLocal變量,用于存儲LocalVariablestatic ThreadLocal<LocalVariable> threadLocal = new ThreadLocal<>();public static void main(String[] args) throws InterruptedException {testCase1();}

3.2.1 第一組測試

僅從線程池獲取線程,不做任何操作

/*** 測試場景1:僅從線程池獲取線程,不做任何操作*/public static void testCase1() throws InterruptedException {System.out.println("執行測試場景1:僅使用線程池,不做任何操作");while (true) {poolExecutor.execute(() -> {System.out.println("線程" + Thread.currentThread().getId() + "執行完畢");try {// 短暫休眠,模擬任務執行時間Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}});// 控制任務提交速度Thread.sleep(50);}}

通過visualvm工具可以看到內存占用很小

3.2.2 第二組測試

創建LocalVariable但不使用ThreadLocal

/*** 測試場景2:創建LocalVariable但不使用ThreadLocal*/
public static void testCase2() throws InterruptedException {
System.out.println("執行測試場景2:創建LocalVariable但不使用ThreadLocal");while (true) {poolExecutor.execute(() -> {// 創建本地變量,但不存儲到ThreadLocalLocalVariable var = new LocalVariable();System.out.println("線程" + Thread.currentThread().getId() + "創建了LocalVariable");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 方法結束后,var會被GC回收});Thread.sleep(50);
}
}

在GC的作用下,無用的對象會被回收,內存占用會下降到正常水平

3.2.3 第三組測試

使用ThreadLocal存儲LocalVariable但不清理

 /*** 測試場景3:使用ThreadLocal存儲LocalVariable但不清理*/public static void testCase3() throws InterruptedException {System.out.println("執行測試場景3:使用ThreadLocal存儲但不清理");while (true) {poolExecutor.execute(() -> {// 存儲變量到ThreadLocal,但不調用remove()threadLocal.set(new LocalVariable());System.out.println("線程" + Thread.currentThread().getId() + "向ThreadLocal存儲了變量");try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 不清理ThreadLocal,可能導致內存泄露});Thread.sleep(50);}}

可以看到,與2不同的是,盡管有GC的作用,但是占用的內存還是超出預料范圍

3.2.4 第四組測試

使用ThreadLocal存儲并正確清理

 /*** 測試場景4:使用ThreadLocal存儲并正確清理*/public static void testCase4() throws InterruptedException {System.out.println("執行測試場景4:使用ThreadLocal存儲并清理");while (true) {poolExecutor.execute(() -> {try {// 存儲變量到ThreadLocalthreadLocal.set(new LocalVariable());System.out.println("線程" + Thread.currentThread().getId() + "向ThreadLocal存儲了變量并清理");Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();} finally {// 手動清理ThreadLocal,避免內存泄露threadLocal.remove();}});Thread.sleep(50);}}

可以看到,在GC的作用下,現象與第二組測試基本一致

3.3 內存泄露的分析

根據我們前面對 ThreadLocal 的分析,我們可以知道每個 Thread 維護一個 ThreadLocalMap,這個映射表的 key 是 ThreadLocal 實例本身, value 是真正需 要存儲的 Object,也就是說 ThreadLocal 本身并不存儲值, 它只是作為一個 key 來讓線程從 ThreadLocalMap 獲取 value。

仔細觀察ThreadLocalMap,這個 map 是使用 ThreadLocal 的弱引用作為 Key 的,弱引用的對象在 GC 時會被回收

那么此時的引用鏈路關系就是:

圖中的虛線表示弱引用

這 樣 , 當 把 threadlocal 變 量 置 為 null 以 后 , 沒有任何強引用指向threadlocal實例 ,只存在key的弱引用, 所 以Threadlocal() 將會被 gc 回收。

這樣一來, ThreadLocalMap 中就會出現 key 為 null 的 Entry,就沒有辦法訪問這些 key 為 null 的 Entry 的 value,如果當前 線程再遲遲不結束的話(如線程池),這些 key 為 null 的Entry 的 value 就會一直存在一條強 引用鏈: Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value ,而這塊 value 永 遠不會被訪問到了,所以存在著內存泄露

只有當前 thread 結束以后, current thread 就不會存在棧中,強引用斷開, Current Thread 、Map

value 將全部被 GC 回收。

最好的做法是不在需要使用ThreadLocal 變量后,都調用它的 remove()方法,清除數據。

從表面上看內存泄漏的根源在于使用了弱引用, 但是另一個問題也同樣值得 思考:為什么使用弱引用而不是強引用?

我們分兩種情況討論:

key 使用強引用: 對 ThreadLocal 對象實例的引用被置為 null 了,但是ThreadLocalMap 還持有這個 ThreadLocal 對象實例的強引用, 如果沒有手動刪除, ThreadLocal 的對象實例不會被回收,導致 Entry 內存泄漏。

key 使用弱引用: 對 ThreadLocal 對象實例的引用被被置為 null 了,由于ThreadLocalMap 持有 ThreadLocal 的弱引用, 即使沒有手動刪除, ThreadLocal 的 對象實例也會被回收。value 在下一次 ThreadLocalMap 調用 set,get ,remove 都 有機會被回收。

比較兩種情況,我們可以發現:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長, 如果都沒有手動刪除對應 key,都會導致內存泄漏, 但是使用弱引用可以多一層保障。

因此, ThreadLocal 內存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長, 如果沒有手動刪除對應 key 就會導致內存泄漏, 而不是因為弱引用。

3.4 總結

JVM 利用設置 ThreadLocalMap 的 Key 為弱引用,來避免內存泄露。 JVM 利用調用 remove 、get、set 方法的時候,回收弱引用。

當 ThreadLocal 存儲很多 Key 為 null 的 Entry 的時候,而不再去調用 remove、 get 、set 方法,那

么將導致內存泄漏。

使用線程池+ ThreadLocal 時要小心, 因為這種情況下, 線程是一直在不斷的 重復運行的,從而也就造成了 value 可能造成累積的情況。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/94175.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/94175.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/94175.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

React組件化的封裝

1. 組件化封裝的結構 1.1. 定義一個類(組件名必須是大寫&#xff0c;小寫會被認為是html元素), 繼續自React.Component1.2. 實現當前組件的render函數 render當中返回的jsx內容&#xff0c;就是之后React會幫助我們渲染的內容 1.3. 結構圖如下&#xff1a; data 方法render()…

嵌入式仿真教學的革新力量:深圳航天科技創新研究院引領高效學習新時代

嵌入式系統作為現代信息技術的核心基石&#xff0c;已深度融入工業控制、物聯網、智能終端等關鍵領域。高校肩負著培養嵌入式技術人才的重任&#xff0c;但傳統教學方式正面臨嚴峻挑戰&#xff1a;硬件實驗設備投入巨大、更新滯后、維護繁瑣、時空限制嚴格&#xff0c;難以滿足…

六、Linux核心服務與包管理

作者&#xff1a;IvanCodes 日期&#xff1a;2025年8月3日 專欄&#xff1a;Linux教程 要保證一個Linux系統穩定、安全、功能完備&#xff0c;有效管理其后臺服務和軟件包是至關重要的。本文將深入介紹現代Linux系統中四個核心的管理工具&#xff1a;systemctl (服務管理)&…

【數據結構】哈希表實現

目錄 1. 哈希概念 2 哈希沖突和哈希函數 3. 負載因子 4. 將關鍵字轉為整數 5. 哈希函數 5.1直接定址法 5.2 除法散列法/除留余數法 5.3 乘法散列法&#xff08;了解&#xff09; 5.4 全域散列法&#xff08;了解&#xff09; 5.5 其他方法&#xff08;了解&#xff09…

PostgreSQL面試題及詳細答案120道(21-40)

《前后端面試題》專欄集合了前后端各個知識模塊的面試題&#xff0c;包括html&#xff0c;javascript&#xff0c;css&#xff0c;vue&#xff0c;react&#xff0c;java&#xff0c;Openlayers&#xff0c;leaflet&#xff0c;cesium&#xff0c;mapboxGL&#xff0c;threejs&…

數據建模及基本數據分析

目錄 &#xff08;一&#xff09;數據建模 1.以數據預測為核心的建模 2.以數據聚類為核心的建模 &#xff08;二&#xff09;基本數據分析 1.Numpy 2. Pandas 3.實例 4.Matplotlib 資料自取&#xff1a; 鏈接: https://pan.baidu.com/s/1PROmz-2hR3VCTd6Eei6lFQ?pwdy8…

電動汽車DCDC轉換器的用途及工作原理

在電動汽車的電氣架構中&#xff0c;DCDC轉換器&#xff08;直流-直流轉換器&#xff09;是一個至關重要的部件&#xff0c;負責協調高壓動力電池&#xff08;通常300V~800V&#xff09;與低壓電氣系統&#xff08;12V/24V&#xff09;之間的能量流動。它的性能直接影響整車的能…

PyTorch 應用于3D 點云數據處理匯總和點云配準示例演示

PyTorch 已廣泛應用于 3D 點云數據處理&#xff0c;特別是在深度學習驅動的任務中如&#xff1a; 分類、分割、配準、重建、姿態估計、SLAM、目標檢測 等。 傳統 3D 點云處理以 PCL、Open3D 為主&#xff0c;深度學習方法中&#xff0c;PyTorch 是構建神經網絡處理點云的核心框…

ABP VNext + Quartz.NET vs Hangfire:靈活調度與任務管理

ABP VNext Quartz.NET vs Hangfire&#xff1a;靈活調度與任務管理 &#x1f680; &#x1f4da; 目錄ABP VNext Quartz.NET vs Hangfire&#xff1a;靈活調度與任務管理 &#x1f680;? TL;DR&#x1f6e0; 環境與依賴&#x1f527; Quartz.NET 在 ABP 中接入1. 安裝與模塊…

[硬件電路-148]:數字電路 - 什么是CMOS電平、TTL電平?還有哪些其他電平標準?發展歷史?

1. CMOS電平定義&#xff1a; CMOS&#xff08;Complementary Metal-Oxide-Semiconductor&#xff09;電平基于互補金屬氧化物半導體工藝&#xff0c;由PMOS和NMOS晶體管組成。其核心特點是低功耗、高抗干擾性和寬電源電壓范圍&#xff08;通常為3V~18V&#xff09;。關鍵參數&…

0基礎網站開發技術教學(二) --(前端篇 2)--

書接上回說到的前端3種主語言以及其用法&#xff0c;這期我們再來探討一下javascript的一些編碼技術。 一) 自定義函數 假如你要使用一個功能&#xff0c;正常來說直接敲出來便可。可如果這個功能你要用不止一次呢?難道你每次都敲出來嗎?這個時侯&#xff0c;就要用到我們的自…

前端 拼多多4399筆試題目

拼多多 3 選擇題 opacity|visibity|display區別 在CSS中&#xff0c;opacity: 0 和 visibility: hidden 都可以讓元素不可見&#xff0c;但它們的行為不同&#xff1a; ? opacity: 0&#xff08;透明度為0&#xff09; 元素仍然占據空間&#xff08;不移除文檔流&#xff0…

數琨創享:全球汽車高端制造企業 QMS質量管理平臺案例

01.行業領軍者的質量升級使命在全球汽車產業鏈加速升級的浪潮中&#xff0c;質量管控能力已成為企業核心競爭力的關鍵。作為工信部認證的制造業單項冠軍示范企業&#xff0c;萬向集團始終以“全球制造、全球市場、做行業領跑者”為戰略愿景。面對奔馳、寶馬、大眾等“9N”高端客…

GaussDB 約束的使用舉例

1 not null 約束not null 約束強制列不接受 null 值。not null 約束強制字段始終包含值。這意味著&#xff0c;如果不向字段添加值&#xff0c;就無法插入新記錄或者更新記錄。GaussDB使用pg_get_tabledef()函數獲取customers表結構&#xff0c;如&#xff1a;csdn> set sea…

自動駕駛中的傳感器技術13——Camera(4)

1、自駕Camera開發的方案是否歸一化對于OEM&#xff0c;或者自駕方案商如Mobileye如果進行Camera的開發&#xff0c;一般建議采用Tesla的系統化最優方案&#xff0c;所有Camera統一某個或者某兩個MP設計&#xff08;增加CIS議價權&#xff0c;減少Camera PCBA的設計維護數量&am…

開源利器:glTF Compressor——高效優化3D模型的終極工具

在3D圖形開發領域,glTF(GL Transmission Format)已成為Web和移動端3D內容的通用標準。然而,3D模型的文件體積和紋理質量往往面臨權衡難題。Shopify最新開源的glTF Compressor工具,為開發者提供了一套精細化、自動化的解決方案,讓3D模型優化既高效又精準。本文將深入解析這…

LeetCode Hot 100,快速學習,不斷更

工作做多了有時候需要回歸本心&#xff0c;認真刷題記憶一下算法。那就用我這練習時長兩年半的代碼農民工來嘗試著快速解析LeetCode 100吧 快速解析 哈希 1. 兩數之和 - 力扣&#xff08;LeetCode&#xff09; 這題很簡單啊&#xff0c;思路也很多 1. 暴力搜索&#xff0c;…

MySQL的子查詢:

目錄 子查詢的相關概念&#xff1a; 子查詢的分類&#xff1a; 角度1&#xff1a; 單行子查詢&#xff1a; 單行比較操作符&#xff1a; 子查詢的空值情況&#xff1a; 多行子查詢&#xff1a; 多行比較操作符&#xff1a; ANY和ALL的區別&#xff1a; 子查詢為空值的…

Python批處理深度解析:構建高效大規模數據處理系統

引言&#xff1a;批處理的現代價值在大數據時代&#xff0c;批處理&#xff08;Batch Processing&#xff09; 作為數據處理的核心范式&#xff0c;正經歷著復興。盡管實時流處理備受關注&#xff0c;但批處理在數據倉庫構建、歷史數據分析、報表生成等場景中仍不可替代。Pytho…

是德科技的BenchVue和納米軟件的ATECLOUD有哪些區別?

是德科技的BenchVue和納米軟件的ATECLOUD雖然都是針對儀器儀表測試的軟件&#xff0c;但是在功能設計、測試場景、技術架構等方面有著明顯的差異。BenchVue&#xff08;是德科技&#xff09;由全球領先的測試測量設備供應商開發&#xff0c;專注于高端儀器控制與數據分析&#…