真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

JVM堆外內(nèi)存泄漏故障排查的示例分析

本篇文章為大家展示了JVM堆外內(nèi)存泄漏故障排查的示例分析,內(nèi)容簡(jiǎn)明扼要并且容易理解,絕對(duì)能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。

創(chuàng)新互聯(lián)建站專注于科爾沁左翼網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠為您提供科爾沁左翼營(yíng)銷型網(wǎng)站建設(shè),科爾沁左翼網(wǎng)站制作、科爾沁左翼網(wǎng)頁設(shè)計(jì)、科爾沁左翼網(wǎng)站官網(wǎng)定制、小程序定制開發(fā)服務(wù),打造科爾沁左翼網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供科爾沁左翼網(wǎng)站排名全網(wǎng)營(yíng)銷落地服務(wù)。

 

前言

記錄一次線上JVM堆外內(nèi)存泄漏問題的排查過程與思路,其中夾帶一些「JVM內(nèi)存分配的原理分析」以及「常用的JVM問題排查手段和工具分享」,希望對(duì)大家有所幫助。

在整個(gè)排查過程中,我也走了不少彎路,但是在文章中我仍然會(huì)把完整的思路和想法寫出來,當(dāng)做一次經(jīng)驗(yàn)教訓(xùn),給后人參考,文章最后也總結(jié)了下內(nèi)存泄漏問題快速排查的幾個(gè)原則。

故障描述

8月12日中午午休時(shí)間,我們商業(yè)服務(wù)收到告警,服務(wù)進(jìn)程占用容器的物理內(nèi)存(16G)超過了80%的閾值,并且還在不斷上升。

JVM堆外內(nèi)存泄漏故障排查的示例分析  

監(jiān)控系統(tǒng)調(diào)出圖表查看:

JVM堆外內(nèi)存泄漏故障排查的示例分析  

像是Java進(jìn)程發(fā)生了內(nèi)存泄漏,而我們堆內(nèi)存的限制是4G,這種大于4G快要吃滿內(nèi)存應(yīng)該是JVM堆外內(nèi)存泄漏。

確認(rèn)了下當(dāng)時(shí)服務(wù)進(jìn)程的啟動(dòng)配置:

-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80
 

雖然當(dāng)天沒有上線新代碼,但是「當(dāng)天上午我們正在使用消息隊(duì)列推送歷史數(shù)據(jù)的修復(fù)腳本,該任務(wù)會(huì)大量調(diào)用我們服務(wù)其中的某一個(gè)接口」,所以初步懷疑和該接口有關(guān)。

下圖是該調(diào)用接口當(dāng)天的訪問量變化:

JVM堆外內(nèi)存泄漏故障排查的示例分析  

可以看到案發(fā)當(dāng)時(shí)調(diào)用量相比正常情況(每分鐘200+次)提高了很多(每分鐘5000+次)。

「我們暫時(shí)讓腳本停止發(fā)送消息,該接口調(diào)用量下降到每分鐘200+次,容器內(nèi)存不再以極高斜率上升,一切似乎恢復(fù)了正常?!?/strong>

接下來排查這個(gè)接口是不是發(fā)生了內(nèi)存泄漏。

 

排查過程

首先我們先回顧下Java進(jìn)程的內(nèi)存分配,方便我們下面排查思路的闡述。

「以我們線上使用的JDK1.8版本為例」。JVM內(nèi)存分配網(wǎng)上有許多總結(jié),我就不再進(jìn)行二次創(chuàng)作。

JVM內(nèi)存區(qū)域的劃分為兩塊:堆區(qū)和非堆區(qū)。

  • 堆區(qū):就是我們熟知的新生代老年代。
  • 非堆區(qū):非堆區(qū)如圖中所示,有元數(shù)據(jù)區(qū)和直接內(nèi)存。
JVM堆外內(nèi)存泄漏故障排查的示例分析  

「這里需要額外注意的是:永久代(JDK8的原生去)存放JVM運(yùn)行時(shí)使用的類,永久代的對(duì)象在full GC時(shí)進(jìn)行垃圾收集?!?/strong>

復(fù)習(xí)完了JVM的內(nèi)存分配,讓我們回到故障上來。

 

堆內(nèi)存分析

雖說一開始就基本確認(rèn)與堆內(nèi)存無關(guān),因?yàn)樾孤兜膬?nèi)存占用超過了堆內(nèi)存限制4G,但是我們?yōu)榱吮kU(xiǎn)起見先看下堆內(nèi)存有什么線索。

我們觀察了新生代和老年代內(nèi)存占用曲線以及回收次數(shù)統(tǒng)計(jì),和往常一樣沒有大問題,我們接著在事故現(xiàn)場(chǎng)的容器上dump了一份JVM堆內(nèi)存的日志。

 
堆內(nèi)存Dump

堆內(nèi)存快照dump命令:

jmap -dump:live,format=b,file=xxxx.hprof pid
 
?  

畫外音:你也可以使用jmap -histo:live pid直接查看堆內(nèi)存存活的對(duì)象。

?  

導(dǎo)出后,將Dump文件下載回本地,然后可以使用Eclipse的MAT(Memory Analyzer)或者JDK自帶的JVisualVM打開日志文件。

使用MAT打開文件如圖所示:

JVM堆外內(nèi)存泄漏故障排查的示例分析  

「可以看到堆內(nèi)存中,有一些nio有關(guān)的大對(duì)象,比如正在接收消息隊(duì)列消息的nioChannel,還有nio.HeapByteBuffer,但是數(shù)量不多,不能作為判斷的依據(jù),先放著觀察下?!?/strong>

下一步,我開始瀏覽該接口代碼,接口內(nèi)部主要邏輯是調(diào)用集團(tuán)的WCS客戶端,將數(shù)據(jù)庫表中數(shù)據(jù)查表后寫入WCS,沒有其他額外邏輯

發(fā)覺沒有什么特殊邏輯后,我開始懷疑WCS客戶端封裝是否存在內(nèi)存泄漏,這樣懷疑的理由是,WCS客戶端底層是由SCF客戶端封裝的,作為RPC框架,其底層通訊傳輸協(xié)議有可能會(huì)申請(qǐng)直接內(nèi)存。

「是不是我的代碼出發(fā)了WCS客戶端的Bug,導(dǎo)致不斷地申請(qǐng)直接內(nèi)存的調(diào)用,最終吃滿內(nèi)存。」

我聯(lián)系上了WCS的值班人,將我們遇到的問題和他們描述了一下,他們回復(fù)我們,會(huì)在他們本地執(zhí)行下寫入操作的壓測(cè),看看能不能復(fù)現(xiàn)我們的問題。

既然等待他們的反饋還需要時(shí)間,我們就準(zhǔn)備先自己琢磨下原因。

「我將懷疑的目光停留在了直接內(nèi)存上,懷疑是由于接口調(diào)用量過大,客戶端對(duì)nio使用不當(dāng),導(dǎo)致使用ByteBuffer申請(qǐng)過多的直接內(nèi)存。」

?  

「畫外音:最終的結(jié)果證明,這一個(gè)先入為主的思路導(dǎo)致排查過程走了彎路。在問題的排查過程中,用合理的猜測(cè)來縮小排查范圍是可以的,但最好先把每種可能性都列清楚,在發(fā)現(xiàn)自己深入某個(gè)可能性無果時(shí),要及時(shí)回頭仔細(xì)審視其他可能性?!?/strong>

?  
 

沙箱環(huán)境復(fù)現(xiàn)

為了能還原當(dāng)時(shí)的故障場(chǎng)景,我在沙箱環(huán)境申請(qǐng)了一臺(tái)壓測(cè)機(jī)器,來確保和線上環(huán)境一致。

「首先我們先模擬內(nèi)存溢出的情況(大量調(diào)用接口):」

我們讓腳本繼續(xù)推送數(shù)據(jù),調(diào)用我們的接口,我們持續(xù)觀察內(nèi)存占用。

當(dāng)開始調(diào)用后,內(nèi)存便開始持續(xù)增長(zhǎng),并且看起來沒有被限制住(沒有因?yàn)橄拗朴|發(fā)Full GC)。

JVM堆外內(nèi)存泄漏故障排查的示例分析  

「接著我們來模擬下平時(shí)正常調(diào)用量的情況(正常量調(diào)用接口):」

我們將該接口平時(shí)正常的調(diào)用量(比較小,且每10分鐘進(jìn)行一次批量調(diào)用)切到該壓測(cè)機(jī)器上,得到了下圖這樣的老生代內(nèi)存和物理內(nèi)存趨勢(shì):

JVM堆外內(nèi)存泄漏故障排查的示例分析  
JVM堆外內(nèi)存泄漏故障排查的示例分析  

「問題來了:為何內(nèi)存會(huì)不斷往上走吃滿內(nèi)存呢?」

當(dāng)時(shí)猜測(cè)是由于JVM進(jìn)程并沒有對(duì)于直接內(nèi)存大小進(jìn)行限制(-XX:MaxDirectMemorySize),所以堆外內(nèi)存不斷上漲,并不會(huì)觸發(fā)FullGC操作。

「上圖能夠得出兩個(gè)結(jié)論:」

  • 在內(nèi)存泄露的接口調(diào)用量很大的時(shí)候,如果恰好堆內(nèi)老生代等其他情況一直不滿足FullGC條件,就一直不會(huì)FullGC,直接內(nèi)存一路上漲。
  • 而在平時(shí)低調(diào)用量的情況下, 內(nèi)存泄漏的比較慢,F(xiàn)ullGC總會(huì)到來,回收掉泄露的那部分,這也是平時(shí)沒有出問題,正常運(yùn)行了很久的原因。

「由于上面提到,我們進(jìn)程的啟動(dòng)參數(shù)中并沒有限制直接內(nèi)存,于是我們將-XX:MaxDirectMemorySize配置加上,再次在沙箱環(huán)境進(jìn)行了測(cè)驗(yàn)。」

結(jié)果發(fā)現(xiàn),進(jìn)程占用的物理內(nèi)存依然會(huì)不斷上漲,超出了我們?cè)O(shè)置的限制,“看上去”配置似乎沒起作用。

這讓我很訝異,難道JVM對(duì)內(nèi)存的限制出現(xiàn)了問題?

「到了這里,能夠看出我排查過程中思路執(zhí)著于直接內(nèi)存的泄露,一去不復(fù)返了。」

?  

「畫外音:我們應(yīng)該相信JVM對(duì)內(nèi)存的掌握,如果發(fā)現(xiàn)參數(shù)失效,多從自己身上找原因,看看是不是自己使用參數(shù)有誤?!?/strong>

?  
 

直接內(nèi)存分析

為了更進(jìn)一步的調(diào)查清楚直接內(nèi)存里有什么,我開始對(duì)直接內(nèi)存下手。由于直接內(nèi)存并不能像堆內(nèi)存一樣,很容易的看出所有占用的對(duì)象,我們需要一些命令來對(duì)直接內(nèi)存進(jìn)行排查,我有用了幾種辦法,來查看直接內(nèi)存里到底出現(xiàn)了什么問題。

 
查看進(jìn)程內(nèi)存信息 pmap

pmap - report memory map of a process(查看進(jìn)程的內(nèi)存映像信息)

pmap命令用于報(bào)告進(jìn)程的內(nèi)存映射關(guān)系,是Linux調(diào)試及運(yùn)維一個(gè)很好的工具。

pmap -x pid 如果需要排序  | sort -n -k3**
 

執(zhí)行后我得到了下面的輸出,刪減輸出如下:

..
00007fa2d4000000    8660    8660    8660 rw---   [ anon ]
00007fa65f12a000    8664    8664    8664 rw---   [ anon ]
00007fa610000000    9840    9832    9832 rw---   [ anon ]
00007fa5f75ff000   10244   10244   10244 rw---   [ anon ]
00007fa6005fe000   59400   10276   10276 rw---   [ anon ]
00007fa3f8000000   10468   10468   10468 rw---   [ anon ]
00007fa60c000000   10480   10480   10480 rw---   [ anon ]
00007fa614000000   10724   10696   10696 rw---   [ anon ]
00007fa6e1c59000   13048   11228       0 r-x-- libjvm.so
00007fa604000000   12140   12016   12016 rw---   [ anon ]
00007fa654000000   13316   13096   13096 rw---   [ anon ]
00007fa618000000   16888   16748   16748 rw---   [ anon ]
00007fa624000000   37504   18756   18756 rw---   [ anon ]
00007fa62c000000   53220   22368   22368 rw---   [ anon ]
00007fa630000000   25128   23648   23648 rw---   [ anon ]
00007fa63c000000   28044   24300   24300 rw---   [ anon ]
00007fa61c000000   42376   27348   27348 rw---   [ anon ]
00007fa628000000   29692   27388   27388 rw---   [ anon ]
00007fa640000000   28016   28016   28016 rw---   [ anon ]
00007fa620000000   28228   28216   28216 rw---   [ anon ]
00007fa634000000   36096   30024   30024 rw---   [ anon ]
00007fa638000000   65516   40128   40128 rw---   [ anon ]
00007fa478000000   46280   46240   46240 rw---   [ anon ]
0000000000f7e000   47980   47856   47856 rw---   [ anon ]
00007fa67ccf0000   52288   51264   51264 rw---   [ anon ]
00007fa6dc000000   65512   63264   63264 rw---   [ anon ]
00007fa6cd000000   71296   68916   68916 rwx--   [ anon ]
00000006c0000000 4359360 2735484 2735484 rw---   [ anon ]
 

可以看出,最下面一行是堆內(nèi)存的映射,占用4G,其他上面有非常多小的內(nèi)存占用,不過通過這些信息我們依然看不出問題。

 
堆外內(nèi)存跟蹤 NativeMemoryTracking
?  

Native Memory Tracking (NMT) 是Hotspot VM用來分析VM內(nèi)部?jī)?nèi)存使用情況的一個(gè)功能。我們可以利用jcmd(jdk自帶)這個(gè)工具來訪問NMT的數(shù)據(jù)。

?  

NMT必須先通過VM啟動(dòng)參數(shù)中打開,不過要注意的是,打開NMT會(huì)帶來5%-10%的性能損耗。

-XX:NativeMemoryTracking=[off | summary | detail]
# off: 默認(rèn)關(guān)閉
# summary: 只統(tǒng)計(jì)各個(gè)分類的內(nèi)存使用情況.
# detail: Collect memory usage by individual call sites.
 

然后運(yùn)行進(jìn)程,可以使用下面的命令查看直接內(nèi)存:

jcmd  VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
 
# summary: 分類內(nèi)存使用情況.
# detail: 詳細(xì)內(nèi)存使用情況,除了summary信息之外還包含了虛擬內(nèi)存使用情況。
# baseline: 創(chuàng)建內(nèi)存使用快照,方便和后面做對(duì)比
# summary.diff: 和上一次baseline的summary對(duì)比
# detail.diff: 和上一次baseline的detail對(duì)比
# shutdown: 關(guān)閉NMT
 

我們使用:

jcmd pid VM.native_memory detail scale=MB > temp.txt
 

得到如圖結(jié)果:

JVM堆外內(nèi)存泄漏故障排查的示例分析  
JVM堆外內(nèi)存泄漏故障排查的示例分析  

上圖中給我們的信息,都不能很明顯的看出問題,至少我當(dāng)時(shí)依然不能通過這幾次信息看出問題。

排查似乎陷入了僵局。

 

山重水復(fù)疑無路

在排查陷入停滯的時(shí)候,我們得到了來自WCS和SCF方面的回復(fù),「兩方都確定了他們的封裝沒有內(nèi)存泄漏的存在」,WCS方面沒有使用直接內(nèi)存,而SCF雖然作為底層RPC協(xié)議,但是也不會(huì)遺留這么明顯的內(nèi)存bug,否則應(yīng)該線上有很多反饋。

 
查看JVM內(nèi)存信息 jmap

此時(shí),找不到問題的我再次新開了一個(gè)沙箱容器,運(yùn)行服務(wù)進(jìn)程,然后運(yùn)行jmap命令,看一看JVM內(nèi)存的「實(shí)際配置」

jmap -heap pid
 

得到結(jié)果:

Attaching to process ID 1474, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.66-b17

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 2147483648 (2048.0MB)
   MaxNewSize               = 2147483648 (2048.0MB)
   OldSize                  = 2147483648 (2048.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 1932787712 (1843.25MB)
   used     = 1698208480 (1619.5378112792969MB)
   free     = 234579232 (223.71218872070312MB)
   87.86316621615607% used
Eden Space:
   capacity = 1718091776 (1638.5MB)
   used     = 1690833680 (1612.504653930664MB)
   free     = 27258096 (25.995346069335938MB)
   98.41346682518548% used
From Space:
   capacity = 214695936 (204.75MB)
   used     = 7374800 (7.0331573486328125MB)
   free     = 207321136 (197.7168426513672MB)
   3.4349974840697497% used
To Space:
   capacity = 214695936 (204.75MB)
   used     = 0 (0.0MB)
   free     = 214695936 (204.75MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 2147483648 (2048.0MB)
   used     = 322602776 (307.6579818725586MB)
   free     = 1824880872 (1740.3420181274414MB)
   15.022362396121025% used

29425 interned Strings occupying 3202824 bytes
 

輸出的信息中,看得出老年代和新生代都蠻正常的,元空間也只占用了20M,直接內(nèi)存看起來也是2g...

嗯?為什么MaxMetaspaceSize = 17592186044415 MB「看起來就和沒限制一樣」。

再仔細(xì)看看我們的啟動(dòng)參數(shù):

-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80
 

配置的是-XX:PermSize=256m -XX:MaxPermSize=512m,也就是永久代的內(nèi)存空間。「而1.8后,Hotspot虛擬機(jī)已經(jīng)移除了永久代,使用了元空間代替?!?/strong>由于我們線上使用的是JDK1.8,「所以我們對(duì)于元空間的最大容量根本就沒有做限制」,-XX:PermSize=256m -XX:MaxPermSize=512m 這兩個(gè)參數(shù)對(duì)于1.8就是過期的參數(shù)。

下面的圖描述了從1.7到1.8,永久代的變更:

JVM堆外內(nèi)存泄漏故障排查的示例分析  

「那會(huì)不會(huì)是元空間內(nèi)存泄露了呢?」

我選擇了在本地進(jìn)行測(cè)試,方便更改參數(shù),也方便使用JVisualVM工具直觀的看出內(nèi)存變化。

 
使用JVisualVM觀察進(jìn)程運(yùn)行

首先限制住元空間,使用參數(shù)-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m,然后在本地循環(huán)調(diào)用出問題的接口。

得到如圖:

JVM堆外內(nèi)存泄漏故障排查的示例分析  

「可以看出,在元空間耗盡時(shí),系統(tǒng)出發(fā)了Full GC,元空間內(nèi)存得到回收,并且卸載了很多類?!?/strong>

然后我們將元空間限制去掉,也就是使用之前出問題的參數(shù):

-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80 -XX:MaxDirectMemorySize=2g -XX:+UnlockDiagnosticVMOptions
 

得到如圖:

JVM堆外內(nèi)存泄漏故障排查的示例分析  

「可以看出,元空間在不斷上漲,并且已裝入的類隨著調(diào)用量的增加也在不斷上漲,呈現(xiàn)正相關(guān)趨勢(shì)?!?/strong>

 

柳暗花明又一村

問題一下子明朗了起來,「隨著每次接口的調(diào)用,極有可能是某個(gè)類都在不斷的被創(chuàng)建,占用了元空間的內(nèi)存」

 
觀察JVM類加載情況 -verbose
?  

在調(diào)試程序時(shí),有時(shí)需要查看程序加載的類、內(nèi)存回收情況、調(diào)用的本地接口等。這時(shí)候就需要-verbose命令。在myeclipse可以通過右鍵設(shè)置(如下),也可以在命令行輸入java -verbose來查看。

?  
-verbose:class 查看類加載情況
-verbose:gc 查看虛擬機(jī)中內(nèi)存回收情況
-verbose:jni 查看本地方法調(diào)用的情況
 

我們?cè)诒镜丨h(huán)境,添加啟動(dòng)參數(shù)-verbose:class循環(huán)調(diào)用接口。

可以看到生成了無數(shù)com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto:

[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
[Loaded com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto from file:/C:/Users/yangzhendong01/.m2/repository/com/alibaba/fastjson/1.2.71/fastjson-1.2.71.jar]
 

當(dāng)調(diào)用了很多次,積攢了一定的類時(shí),我們手動(dòng)執(zhí)行Full GC,進(jìn)行類加載器的回收,我們發(fā)現(xiàn)大量的fastjson相關(guān)類被回收。

JVM堆外內(nèi)存泄漏故障排查的示例分析  

「如果在回收前,使用jmap查看類加載情況,同樣也可以發(fā)現(xiàn)大量的fastjson相關(guān)類:」

jmap -clstats 7984
 
JVM堆外內(nèi)存泄漏故障排查的示例分析  

這下有了方向,「這次仔細(xì)排查代碼」,查看代碼邏輯里哪里用到了fastjson,發(fā)現(xiàn)了如下代碼:

/**
 * 返回Json字符串.駝峰轉(zhuǎn)_
 * @param bean 實(shí)體類.
 */
public static String buildData(Object bean) {
    try {
        SerializeConfig CONFIG = new SerializeConfig();
        CONFIG.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
        return jsonString = JSON.toJSONString(bean, CONFIG);
    } catch (Exception e) {
        return null;
    }
}
   

問題根因

我們?cè)谡{(diào)用wcs前將駝峰字段的實(shí)體類序列化成下劃線字段,**這需要使用fastjson的SerializeConfig,而我們?cè)陟o態(tài)方法中對(duì)其進(jìn)行了實(shí)例化。SerializeConfig創(chuàng)建時(shí)默認(rèn)會(huì)創(chuàng)建一個(gè)ASM代理類用來實(shí)現(xiàn)對(duì)目標(biāo)對(duì)象的序列化。也就是上面被頻繁創(chuàng)建的類com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto,如果我們復(fù)用SerializeConfig,fastjson會(huì)去尋找已經(jīng)創(chuàng)建的代理類,從而復(fù)用。但是如果new SerializeConfig(),則找不到原來生成的代理類,就會(huì)一直去生成新的WlkCustomerDto代理類。

下面兩張圖時(shí)問題定位的源碼:

JVM堆外內(nèi)存泄漏故障排查的示例分析  
JVM堆外內(nèi)存泄漏故障排查的示例分析  

我們將SerializeConfig作為類的靜態(tài)變量,問題得到了解決。

private static final SerializeConfig CONFIG = new SerializeConfig();

static {
    CONFIG.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
}
   
fastjson SerializeConfig 做了什么

SerializeConfig介紹:

?  

SerializeConfig的主要功能是配置并記錄每種Java類型對(duì)應(yīng)的序列化類(ObjectSerializer接口的實(shí)現(xiàn)類),比如Boolean.class使用BooleanCodec(看命名就知道該類將序列化和反序列化實(shí)現(xiàn)寫到一起了)作為序列化實(shí)現(xiàn)類,float[].class使用FloatArraySerializer作為序列化實(shí)現(xiàn)類。這些序列化實(shí)現(xiàn)類,有的是FastJSON中默認(rèn)實(shí)現(xiàn)的(比如Java基本類),有的是通過ASM框架生成的(比如用戶自定義類),有的甚至是用戶自定義的序列化類(比如Date類型框架默認(rèn)實(shí)現(xiàn)是轉(zhuǎn)為毫秒,應(yīng)用需要轉(zhuǎn)為秒)。當(dāng)然,這就涉及到是使用ASM生成序列化類還是使用JavaBean的序列化類類序列化的問題,這里判斷根據(jù)就是是否Android環(huán)境(環(huán)境變量"java.vm.name"為"dalvik"或"lemur"就是Android環(huán)境),但判斷不僅這里一處,后續(xù)還有更具體的判斷。

?  

理論上來說,每個(gè)SerializeConfig實(shí)例若序列化相同的類,都會(huì)找到之前生成的該類的代理類,來進(jìn)行序列化。們的服務(wù)在每次接口被調(diào)用時(shí),都實(shí)例化一個(gè)ParseConfig對(duì)象來配置Fastjson反序列的設(shè)置,而未禁用ASM代理的情況下,由于每次調(diào)用ParseConfig都是一個(gè)新的實(shí)例,因此永遠(yuǎn)也檢查不到已經(jīng)創(chuàng)建的代理類,所以Fastjson便不斷的創(chuàng)建新的代理類,并加載到metaspace中,最終導(dǎo)致metaspace不斷擴(kuò)張,將機(jī)器的內(nèi)存耗盡。

 
升級(jí)JDK1.8才會(huì)出現(xiàn)問題

導(dǎo)致問題發(fā)生的原因還是值得重視。為什么在升級(jí)之前不會(huì)出現(xiàn)這個(gè)問題?這就要分析jdk1.8和1.7自帶的hotspot虛擬機(jī)的差異了。

?  

從jdk1.8開始,自帶的hostspot虛擬機(jī)取消了過去的永久區(qū),而新增了metaspace區(qū),從功能上看,metaspace可以認(rèn)為和永久區(qū)類似,其最主要的功用也是存放類元數(shù)據(jù),但實(shí)際的機(jī)制則有較大的不同。

首先,metaspace默認(rèn)的最大值是整個(gè)機(jī)器的物理內(nèi)存大小,所以metaspace不斷擴(kuò)張會(huì)導(dǎo)致java程序侵占系統(tǒng)可用內(nèi)存,最終系統(tǒng)沒有可用的內(nèi)存;而永久區(qū)則有固定的默認(rèn)大小,不會(huì)擴(kuò)張到整個(gè)機(jī)器的可用內(nèi)存。當(dāng)分配的內(nèi)存耗盡時(shí),兩者均會(huì)觸發(fā)full gc,但不同的是永久區(qū)在full gc時(shí),以堆內(nèi)存回收時(shí)類似的機(jī)制去回收永久區(qū)中的類元數(shù)據(jù)(Class對(duì)象),只要是根引用無法到達(dá)的對(duì)象就可以回收掉,而metaspace判斷類元數(shù)據(jù)是否可以回收,是根據(jù)加載這些類元數(shù)據(jù)的Classloader是否可以回收來判斷的,只要Classloader不能回收,通過其加載的類元數(shù)據(jù)就不會(huì)被回收。這也就解釋了我們這兩個(gè)服務(wù)為什么在升級(jí)到1.8之后才出現(xiàn)問題,因?yàn)樵谥暗膉dk版本中,雖然每次調(diào)用fastjson都創(chuàng)建了很多代理類,在永久區(qū)中加載類很多代理類的Class實(shí)例,但這些Class實(shí)例都是在方法調(diào)用是創(chuàng)建的,調(diào)用完成之后就不可達(dá)了,因此永久區(qū)內(nèi)存滿了觸發(fā)full gc時(shí),都會(huì)被回收掉。

而使用1.8時(shí),因?yàn)檫@些代理類都是通過主線程的Classloader加載的,這個(gè)Classloader在程序運(yùn)行的過程中永遠(yuǎn)也不會(huì)被回收,因此通過其加載的這些代理類也永遠(yuǎn)不會(huì)被回收,這就導(dǎo)致metaspace不斷擴(kuò)張,最終耗盡機(jī)器的內(nèi)存了。

?  

這個(gè)問題并不局限于fastjson,只要是需要通過程序加載創(chuàng)建類的地方,就有可能出現(xiàn)這種問題。「尤其是在框架中,往往大量采用類似ASM、javassist等工具進(jìn)行字節(jié)碼增強(qiáng),而根據(jù)上面的分析,在jdk1.8之前,因?yàn)榇蠖鄶?shù)情況下動(dòng)態(tài)加載的Class都能夠在full gc時(shí)得到回收,因此不容易出現(xiàn)問題」,也因此很多框架、工具包并沒有針對(duì)這個(gè)問題做一些處理,一旦升級(jí)到1.8之后,這些問題就可能會(huì)暴露出來。

 

問題解決了,接下來復(fù)盤下整個(gè)排查問題的流程,整個(gè)流程暴露了我很多問題,最主要的就是「對(duì)于JVM不同版本的內(nèi)存分配還不夠熟悉」,導(dǎo)致了對(duì)于老生代和元空間判斷失誤,走了很多彎路,在直接內(nèi)存中排查了很久,浪費(fèi)了很多時(shí)間。

其次,排查需要的「一是仔細(xì),二是全面,」,最好將所有可能性先行整理好,不然很容易陷入自己設(shè)定好的排查范圍內(nèi),走進(jìn)死胡同不出來。

最后,總結(jié)一下這次的問題帶來的收獲:

  • JDK1.8開始,自帶的hostspot虛擬機(jī)取消了過去的永久區(qū),而新增了metaspace區(qū),從功能上看,metaspace可以認(rèn)為和永久區(qū)類似,其最主要的功用也是存放類元數(shù)據(jù),但實(shí)際的機(jī)制則有較大的不同。
  • 對(duì)于JVM里面的內(nèi)存需要在啟動(dòng)時(shí)進(jìn)行限制,包括我們熟悉的堆內(nèi)存,也要包括直接內(nèi)存和元生區(qū),這是保證線上服務(wù)正常運(yùn)行最后的兜底。
  • 使用類庫,請(qǐng)多注意代碼的寫法,盡量不要出現(xiàn)明顯的內(nèi)存泄漏。
  • 對(duì)于使用了ASM等字節(jié)碼增強(qiáng)工具的類庫,在使用他們時(shí)請(qǐng)多加小心(尤其是JDK1.8以后)。

上述內(nèi)容就是JVM堆外內(nèi)存泄漏故障排查的示例分析,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。


本文標(biāo)題:JVM堆外內(nèi)存泄漏故障排查的示例分析
路徑分享:http://weahome.cn/article/gidido.html

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部