隔壁同事大佬
前段時間生產系統突然假死,重啓後排查問題時發現是內存溢出(OutOfMemoryError: Java heap space),還好有配置自動生成快照文件。之前只有本地用玩具代碼模擬內存泄露或者是那些一眼就能看出來的問題,所以特意把這次的排查過程記錄下來。項目使用的springcloud。
-
先把快照文件下到本地,然後用Jprofiler工具打開快照文件,看到如下圖
1.png
- 圖中
char[]
數組非常大,佔據了總大小的65%左右。因此右擊char[]
,選中Use Selected Objects
,進入到Biggest Objects
界面,如下圖2.png
- 配合着
Jdk的bin目錄下的jvisualvm.exe
工具,發現這個看起來像FeignClients調用接口的返回結果有5000多個且都是131KB大小,佔據了char[]
數組大小的90%,並且發現裏面有一個風格的報文佔了4000多個。於是開始分析該報文的引用鏈,打開References
界面,切換成Incoming references
,如下圖3.png
- 可以看到這個對象是放在
ThreadLocal
對象中的,這個放入的操作來自於線程池創建的線程,並且所有這些線程的狀態都處於waiting,但是又看不出來這個ThreadLocal
對象是屬於哪個類下面的。於是分析項目源碼,發現項目中並沒有顯示的把接口返回結果放入到ThreadLocal
中去,當時真的傻眼了...這看源碼都沒頭緒。浪費了好長一段時間,突然想到可以使用IDEA強大的Debug功能來幫助我找到這個放入到ThreadLocal
的操作是誰幹的,IDEA可以直接在源碼上調試斷點。於是我使用本地環境,在ThreadLocal
的set方法上加了條件斷點,最終找到了調用鏈,如下圖4.png
- 開始分析源碼,發現FeignClients在解析返回結果時使用了
Fastjson
工具包,Fastjson
的一個優點是解析速度非常快,使用ThreadLocal
存放序列化後的char[]
數組,避免重複分配內存空間。然後又看了下FeignClients使用Fastjson
的地方,源碼顯示是將接口返回的流解析成相應的對象。源碼如下圖5.png
6.png
- 現在找到了
ThreadLocal
和接口響應結果的關聯關係,但是還沒找到爲什麼會內存泄露的原因。一開始看到ThreadLocal
時第一反應是ThreadLocal
的null key導致的內存泄露問題,但是看源碼時發現Jdk1.8已經在set等操作時將key爲null的Entry對象過濾了一遍,使其可以被回收,避免了該問題。於是看那個異常4000次的接口的所有調用場景,每次調用接口都創建一個新的線程池,然後提交調用接口的業務邏輯job。於是我寫了一個玩具代碼來模擬項目中的這個數據流轉場景,發現這種寫法使用Fastjson
時,並且主動讓線程等待可以復現生產上的這個現象,因爲線程沒結束,所以放在ThreadLocal
中的value自然無法被回收。如下圖玩具代碼示例7.png
現在問題找到了,把線程池的寫法改一下就行。
但是爲什麼這樣寫會導致線程池創建的線程處於waiting而不結束的原因還沒理的明白...跪求大佬能留言解釋一下
根據測試結果得到的線程處於waiting的原因是因爲創建的線程池的核心線程數爲1,而線程池的設計的是如果運行的任務小於核心線程數時會調用阻塞隊列的take()
阻塞方法,於是導致線程處於waiting而無法被回收,因此當前線程的ThreadLocal.ThreadLocalMap.Entry
對象無法被回收。
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take(); <——此處調用隊列的阻塞方法
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}