前言
記錄一次線上JVM堆外內(nèi)存泄漏問題的排查過程與思路,其中夾帶一些JVM內(nèi)存分配機制以及常用的JVM問題排查指令和工具分享,希望對大家有所幫助。
在整個排查過程中助泽,我也走了不少彎路,但是在文章中我仍然會把完整的思路和想法寫出來嚎京,當做一次經(jīng)驗教訓(xùn)嗡贺,給后人參考,文章最后也總結(jié)了下內(nèi)存泄漏問題快速排查的幾個原則挖藏。
本文的主要內(nèi)容:
- 故障描述和排查過程
- 故障原因和解決方案分析
- JVM堆內(nèi)內(nèi)存和堆外內(nèi)存分配原理
- 常用的進程內(nèi)存泄漏排查指令和工具介紹和使用
文章撰寫不易暑刃,請大家多多支持我的原創(chuàng)技術(shù)公眾號:后端技術(shù)漫談
故障描述
8月12日中午午休時間厢漩,我們商業(yè)服務(wù)收到告警膜眠,服務(wù)進程占用容器的物理內(nèi)存(16G)超過了80%的閾值,并且還在不斷上升溜嗜。
監(jiān)控系統(tǒng)調(diào)出圖表查看:
像是Java進程發(fā)生了內(nèi)存泄漏宵膨,而我們堆內(nèi)存的限制是4G,這種大于4G快要吃滿內(nèi)存應(yīng)該是JVM堆外內(nèi)存泄漏炸宵。
確認了下當時服務(wù)進程的啟動配置:
-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:PermSize=256m -XX:MaxPermSize=512m -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80
雖然當天沒有上線新代碼辟躏,但是當天上午我們正在使用消息隊列推送歷史數(shù)據(jù)的修復(fù)腳本,該任務(wù)會大量調(diào)用我們服務(wù)其中的某一個接口土全,所以初步懷疑和該接口有關(guān)捎琐。
下圖是該調(diào)用接口當天的訪問量變化:
可以看到案發(fā)當時調(diào)用量相比正常情況(每分鐘200+次)提高了很多(每分鐘5000+次)会涎。
我們暫時讓腳本停止發(fā)送消息,該接口調(diào)用量下降到每分鐘200+次瑞凑,容器內(nèi)存不再以極高斜率上升末秃,一切似乎恢復(fù)了正常。
接下來排查這個接口是不是發(fā)生了內(nèi)存泄漏籽御。
排查過程
首先我們先回顧下Java進程的內(nèi)存分配练慕,方便我們下面排查思路的闡述。
以我們線上使用的JDK1.8版本為例技掏。JVM內(nèi)存分配網(wǎng)上有許多總結(jié)铃将,我就不再進行二次創(chuàng)作。
JVM內(nèi)存區(qū)域的劃分為兩塊:堆區(qū)和非堆區(qū)哑梳。
- 堆區(qū):就是我們熟知的新生代老年代劲阎。
- 非堆區(qū):非堆區(qū)如圖中所示,有元數(shù)據(jù)區(qū)和直接內(nèi)存鸠真。
這里需要額外注意的是:永久代(JDK8的原生去)存放JVM運行時使用的類哪工,永久代的對象在full GC時進行垃圾收集。
復(fù)習(xí)完了JVM的內(nèi)存分配弧哎,讓我們回到故障上來雁比。
堆內(nèi)存分析
雖說一開始就基本確認與堆內(nèi)存無關(guān),因為泄露的內(nèi)存占用超過了堆內(nèi)存限制4G撤嫩,但是我們?yōu)榱吮kU起見先看下堆內(nèi)存有什么線索偎捎。
我們觀察了新生代和老年代內(nèi)存占用曲線以及回收次數(shù)統(tǒng)計,和往常一樣沒有大問題序攘,我們接著在事故現(xiàn)場的容器上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)存存活的對象。
導(dǎo)出后程奠,將Dump文件下載回本地丈牢,然后可以使用Eclipse的MAT(Memory Analyzer)或者JDK自帶的JVisualVM打開日志文件。
使用MAT打開文件如圖所示:
可以看到堆內(nèi)存中瞄沙,有一些nio有關(guān)的大對象己沛,比如正在接收消息隊列消息的nioChannel,還有nio.HeapByteBuffer距境,但是數(shù)量不多申尼,不能作為判斷的依據(jù),先放著觀察下垫桂。
下一步师幕,我開始瀏覽該接口代碼,接口內(nèi)部主要邏輯是調(diào)用集團的WCS客戶端诬滩,將數(shù)據(jù)庫表中數(shù)據(jù)查表后寫入WCS霹粥,沒有其他額外邏輯
發(fā)覺沒有什么特殊邏輯后灭将,我開始懷疑WCS客戶端封裝是否存在內(nèi)存泄漏,這樣懷疑的理由是后控,WCS客戶端底層是由SCF客戶端封裝的宗侦,作為RPC框架,其底層通訊傳輸協(xié)議有可能會申請直接內(nèi)存忆蚀。
是不是我的代碼出發(fā)了WCS客戶端的Bug矾利,導(dǎo)致不斷地申請直接內(nèi)存的調(diào)用,最終吃滿內(nèi)存馋袜。
我聯(lián)系上了WCS的值班人男旗,將我們遇到的問題和他們描述了一下,他們回復(fù)我們欣鳖,會在他們本地執(zhí)行下寫入操作的壓測察皇,看看能不能復(fù)現(xiàn)我們的問題。
既然等待他們的反饋還需要時間泽台,我們就準備先自己琢磨下原因什荣。
我將懷疑的目光停留在了直接內(nèi)存上,懷疑是由于接口調(diào)用量過大怀酷,客戶端對nio使用不當稻爬,導(dǎo)致使用ByteBuffer申請過多的直接內(nèi)存。
畫外音:最終的結(jié)果證明蜕依,這一個先入為主的思路導(dǎo)致排查過程走了彎路桅锄。在問題的排查過程中,用合理的猜測來縮小排查范圍是可以的样眠,但最好先把每種可能性都列清楚友瘤,在發(fā)現(xiàn)自己深入某個可能性無果時,要及時回頭仔細審視其他可能性檐束。
沙箱環(huán)境復(fù)現(xiàn)
為了能還原當時的故障場景辫秧,我在沙箱環(huán)境申請了一臺壓測機器,來確保和線上環(huán)境一致被丧。
首先我們先模擬內(nèi)存溢出的情況(大量調(diào)用接口):
我們讓腳本繼續(xù)推送數(shù)據(jù)盟戏,調(diào)用我們的接口,我們持續(xù)觀察內(nèi)存占用晚碾。
當開始調(diào)用后抓半,內(nèi)存便開始持續(xù)增長喂急,并且看起來沒有被限制赘襦摇(沒有因為限制觸發(fā)Full GC)。
接著我們來模擬下平時正常調(diào)用量的情況(正常量調(diào)用接口):
我們將該接口平時正常的調(diào)用量(比較小廊移,且每10分鐘進行一次批量調(diào)用)切到該壓測機器上糕簿,得到了下圖這樣的老生代內(nèi)存和物理內(nèi)存趨勢:
問題來了:為何內(nèi)存會不斷往上走吃滿內(nèi)存呢探入?
當時猜測是由于JVM進程并沒有對于直接內(nèi)存大小進行限制(-XX:MaxDirectMemorySize),所以堆外內(nèi)存不斷上漲懂诗,并不會觸發(fā)FullGC操作蜂嗽。
上圖能夠得出兩個結(jié)論:
- 在內(nèi)存泄露的接口調(diào)用量很大的時候,如果恰好堆內(nèi)老生代等其他情況一直不滿足FullGC條件殃恒,就一直不會FullGC植旧,直接內(nèi)存一路上漲。
- 而在平時低調(diào)用量的情況下离唐, 內(nèi)存泄漏的比較慢病附,F(xiàn)ullGC總會到來,回收掉泄露的那部分亥鬓,這也是平時沒有出問題完沪,正常運行了很久的原因。
由于上面提到嵌戈,我們進程的啟動參數(shù)中并沒有限制直接內(nèi)存覆积,于是我們將-XX:MaxDirectMemorySize配置加上,再次在沙箱環(huán)境進行了測驗熟呛。
結(jié)果發(fā)現(xiàn)宽档,進程占用的物理內(nèi)存依然會不斷上漲,超出了我們設(shè)置的限制庵朝,“看上去”配置似乎沒起作用雌贱。
這讓我很訝異,難道JVM對內(nèi)存的限制出現(xiàn)了問題偿短?
到了這里欣孤,能夠看出我排查過程中思路執(zhí)著于直接內(nèi)存的泄露,一去不復(fù)返了昔逗。
畫外音:我們應(yīng)該相信JVM對內(nèi)存的掌握降传,如果發(fā)現(xiàn)參數(shù)失效,多從自己身上找原因勾怒,看看是不是自己使用參數(shù)有誤婆排。
直接內(nèi)存分析
為了更進一步的調(diào)查清楚直接內(nèi)存里有什么,我開始對直接內(nèi)存下手笔链。由于直接內(nèi)存并不能像堆內(nèi)存一樣段只,很容易的看出所有占用的對象,我們需要一些命令來對直接內(nèi)存進行排查鉴扫,我有用了幾種辦法赞枕,來查看直接內(nèi)存里到底出現(xiàn)了什么問題。
查看進程內(nèi)存信息 pmap
pmap - report memory map of a process(查看進程的內(nèi)存映像信息)
pmap命令用于報告進程的內(nèi)存映射關(guān)系,是Linux調(diào)試及運維一個很好的工具炕婶。
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)部內(nèi)存使用情況的一個功能枪狂。我們可以利用jcmd(jdk自帶)這個工具來訪問NMT的數(shù)據(jù)。
NMT必須先通過VM啟動參數(shù)中打開宋渔,不過要注意的是摘完,打開NMT會帶來5%-10%的性能損耗。
-XX:NativeMemoryTracking=[off | summary | detail]
# off: 默認關(guān)閉
# summary: 只統(tǒng)計各個分類的內(nèi)存使用情況.
# detail: Collect memory usage by individual call sites.
然后運行進程傻谁,可以使用下面的命令查看直接內(nèi)存:
jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
# summary: 分類內(nèi)存使用情況.
# detail: 詳細內(nèi)存使用情況孝治,除了summary信息之外還包含了虛擬內(nèi)存使用情況。
# baseline: 創(chuàng)建內(nèi)存使用快照审磁,方便和后面做對比
# summary.diff: 和上一次baseline的summary對比
# detail.diff: 和上一次baseline的detail對比
# shutdown: 關(guān)閉NMT
我們使用:
jcmd pid VM.native_memory detail scale=MB > temp.txt
得到如圖結(jié)果:
上圖中給我們的信息谈飒,都不能很明顯的看出問題,至少我當時依然不能通過這幾次信息看出問題态蒂。
排查似乎陷入了僵局杭措。
山重水復(fù)疑無路
在排查陷入停滯的時候,我們得到了來自WCS和SCF方面的回復(fù)钾恢,兩方都確定了他們的封裝沒有內(nèi)存泄漏的存在手素,WCS方面沒有使用直接內(nèi)存,而SCF雖然作為底層RPC協(xié)議瘩蚪,但是也不會遺留這么明顯的內(nèi)存bug泉懦,否則應(yīng)該線上有很多反饋。
查看JVM內(nèi)存信息 jmap
此時疹瘦,找不到問題的我再次新開了一個沙箱容器崩哩,運行服務(wù)進程,然后運行jmap命令言沐,看一看JVM內(nèi)存的實際配置:
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
?看起來就和沒限制一樣起便。
再仔細看看我們的啟動參數(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īng)移除了永久代鸟悴,使用了元空間代替陈辱。 由于我們線上使用的是JDK1.8奖年,所以我們對于元空間的最大容量根本就沒有做限制细诸,-XX:PermSize=256m -XX:MaxPermSize=512m
這兩個參數(shù)對于1.8就是過期的參數(shù)。
下面的圖描述了從1.7到1.8陋守,永久代的變更:
那會不會是元空間內(nèi)存泄露了呢震贵?
我選擇了在本地進行測試,方便更改參數(shù)水评,也方便使用JVisualVM工具直觀的看出內(nèi)存變化猩系。
使用JVisualVM觀察進程運行
首先限制住元空間,使用參數(shù)-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=128m
中燥,然后在本地循環(huán)調(diào)用出問題的接口寇甸。
得到如圖:
可以看出,在元空間耗盡時疗涉,系統(tǒng)出發(fā)了Full GC拿霉,元空間內(nèi)存得到回收,并且卸載了很多類咱扣。
然后我們將元空間限制去掉绽淘,也就是使用之前出問題的參數(shù):
-Xms4g -Xmx4g -Xmn2g -Xss1024K -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=80 -XX:MaxDirectMemorySize=2g -XX:+UnlockDiagnosticVMOptions
得到如圖:
可以看出,元空間在不斷上漲闹伪,并且已裝入的類隨著調(diào)用量的增加也在不斷上漲沪铭,呈現(xiàn)正相關(guān)趨勢。
柳暗花明又一村
問題一下子明朗了起來偏瓤,隨著每次接口的調(diào)用杀怠,極有可能是某個類都在不斷的被創(chuàng)建,占用了元空間的內(nèi)存厅克。
觀察JVM類加載情況 -verbose
在調(diào)試程序時驮肉,有時需要查看程序加載的類、內(nèi)存回收情況已骇、調(diào)用的本地接口等离钝。這時候就需要-verbose命令。在myeclipse可以通過右鍵設(shè)置(如下)褪储,也可以在命令行輸入java -verbose來查看卵渴。
-verbose:class 查看類加載情況
-verbose:gc 查看虛擬機中內(nèi)存回收情況
-verbose:jni 查看本地方法調(diào)用的情況
我們在本地環(huán)境,添加啟動參數(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]
當調(diào)用了很多次浪读,積攢了一定的類時昔榴,我們手動執(zhí)行Full GC,進行類加載器的回收碘橘,我們發(fā)現(xiàn)大量的fastjson相關(guān)類被回收互订。
如果在回收前,使用jmap查看類加載情況痘拆,同樣也可以發(fā)現(xiàn)大量的fastjson相關(guān)類:
jmap -clstats 7984
這下有了方向仰禽,這次仔細排查代碼,查看代碼邏輯里哪里用到了fastjson纺蛆,發(fā)現(xiàn)了如下代碼:
/**
* 返回Json字符串.駝峰轉(zhuǎn)_
* @param bean 實體類.
*/
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;
}
}
問題根因
我們在調(diào)用wcs前將駝峰字段的實體類序列化成下劃線字段吐葵,**這需要使用fastjson的SerializeConfig,而我們在靜態(tài)方法中對其進行了實例化桥氏。SerializeConfig創(chuàng)建時默認會創(chuàng)建一個ASM代理類用來實現(xiàn)對目標對象的序列化温峭。也就是上面被頻繁創(chuàng)建的類com.alibaba.fastjson.serializer.ASMSerializer_1_WlkCustomerDto
,如果我們復(fù)用SerializeConfig,fastjson會去尋找已經(jīng)創(chuàng)建的代理類字支,從而復(fù)用凤藏。但是如果new SerializeConfig(),則找不到原來生成的代理類堕伪,就會一直去生成新的WlkCustomerDto代理類揖庄。
下面兩張圖時問題定位的源碼:
我們將SerializeConfig作為類的靜態(tài)變量,問題得到了解決刃跛。
private static final SerializeConfig CONFIG = new SerializeConfig();
static {
CONFIG.propertyNamingStrategy = PropertyNamingStrategy.SnakeCase;
}
fastjson SerializeConfig 做了什么
SerializeConfig介紹:
SerializeConfig的主要功能是配置并記錄每種Java類型對應(yīng)的序列化類(ObjectSerializer接口的實現(xiàn)類)抠艾,比如Boolean.class使用BooleanCodec(看命名就知道該類將序列化和反序列化實現(xiàn)寫到一起了)作為序列化實現(xiàn)類,float[].class使用FloatArraySerializer作為序列化實現(xiàn)類桨昙。這些序列化實現(xiàn)類检号,有的是FastJSON中默認實現(xiàn)的(比如Java基本類),有的是通過ASM框架生成的(比如用戶自定義類)蛙酪,有的甚至是用戶自定義的序列化類(比如Date類型框架默認實現(xiàn)是轉(zhuǎn)為毫秒齐苛,應(yīng)用需要轉(zhuǎn)為秒)。當然桂塞,這就涉及到是使用ASM生成序列化類還是使用JavaBean的序列化類類序列化的問題凹蜂,這里判斷根據(jù)就是是否Android環(huán)境(環(huán)境變量"java.vm.name"為"dalvik"或"lemur"就是Android環(huán)境),但判斷不僅這里一處阁危,后續(xù)還有更具體的判斷玛痊。
理論上來說,每個SerializeConfig實例若序列化相同的類狂打,都會找到之前生成的該類的代理類擂煞,來進行序列化。們的服務(wù)在每次接口被調(diào)用時趴乡,都實例化一個ParseConfig對象來配置Fastjson反序列的設(shè)置对省,而未禁用ASM代理的情況下蝗拿,由于每次調(diào)用ParseConfig都是一個新的實例,因此永遠也檢查不到已經(jīng)創(chuàng)建的代理類蒿涎,所以Fastjson便不斷的創(chuàng)建新的代理類哀托,并加載到metaspace中,最終導(dǎo)致metaspace不斷擴張劳秋,將機器的內(nèi)存耗盡仓手。
升級JDK1.8才會出現(xiàn)問題
導(dǎo)致問題發(fā)生的原因還是值得重視。為什么在升級之前不會出現(xiàn)這個問題俗批?這就要分析jdk1.8和1.7自帶的hotspot虛擬機的差異了俗或。
從jdk1.8開始市怎,自帶的hostspot虛擬機取消了過去的永久區(qū)岁忘,而新增了metaspace區(qū),從功能上看区匠,metaspace可以認為和永久區(qū)類似干像,其最主要的功用也是存放類元數(shù)據(jù),但實際的機制則有較大的不同驰弄。
首先麻汰,metaspace默認的最大值是整個機器的物理內(nèi)存大小,所以metaspace不斷擴張會導(dǎo)致java程序侵占系統(tǒng)可用內(nèi)存戚篙,最終系統(tǒng)沒有可用的內(nèi)存五鲫;而永久區(qū)則有固定的默認大小,不會擴張到整個機器的可用內(nèi)存岔擂。當分配的內(nèi)存耗盡時位喂,兩者均會觸發(fā)full gc,但不同的是永久區(qū)在full gc時乱灵,以堆內(nèi)存回收時類似的機制去回收永久區(qū)中的類元數(shù)據(jù)(Class對象)塑崖,只要是根引用無法到達的對象就可以回收掉,而metaspace判斷類元數(shù)據(jù)是否可以回收痛倚,是根據(jù)加載這些類元數(shù)據(jù)的Classloader是否可以回收來判斷的规婆,只要Classloader不能回收,通過其加載的類元數(shù)據(jù)就不會被回收蝉稳。這也就解釋了我們這兩個服務(wù)為什么在升級到1.8之后才出現(xiàn)問題抒蚜,因為在之前的jdk版本中,雖然每次調(diào)用fastjson都創(chuàng)建了很多代理類耘戚,在永久區(qū)中加載類很多代理類的Class實例嗡髓,但這些Class實例都是在方法調(diào)用是創(chuàng)建的,調(diào)用完成之后就不可達了毕莱,因此永久區(qū)內(nèi)存滿了觸發(fā)full gc時器贩,都會被回收掉颅夺。
而使用1.8時,因為這些代理類都是通過主線程的Classloader加載的蛹稍,這個Classloader在程序運行的過程中永遠也不會被回收吧黄,因此通過其加載的這些代理類也永遠不會被回收,這就導(dǎo)致metaspace不斷擴張唆姐,最終耗盡機器的內(nèi)存了拗慨。
這個問題并不局限于fastjson,只要是需要通過程序加載創(chuàng)建類的地方奉芦,就有可能出現(xiàn)這種問題赵抢。尤其是在框架中,往往大量采用類似ASM声功、javassist等工具進行字節(jié)碼增強烦却,而根據(jù)上面的分析,在jdk1.8之前先巴,因為大多數(shù)情況下動態(tài)加載的Class都能夠在full gc時得到回收其爵,因此不容易出現(xiàn)問題,也因此很多框架伸蚯、工具包并沒有針對這個問題做一些處理摩渺,一旦升級到1.8之后,這些問題就可能會暴露出來剂邮。
總結(jié)
問題解決了摇幻,接下來復(fù)盤下整個排查問題的流程,整個流程暴露了我很多問題挥萌,最主要的就是對于JVM不同版本的內(nèi)存分配還不夠熟悉绰姻,導(dǎo)致了對于老生代和元空間判斷失誤,走了很多彎路瑞眼,在直接內(nèi)存中排查了很久龙宏,浪費了很多時間。
其次伤疙,排查需要的一是仔細银酗,二是全面,徒像,最好將所有可能性先行整理好黍特,不然很容易陷入自己設(shè)定好的排查范圍內(nèi),走進死胡同不出來锯蛀。
最后灭衷,總結(jié)一下這次的問題帶來的收獲:
- JDK1.8開始,自帶的hostspot虛擬機取消了過去的永久區(qū)旁涤,而新增了metaspace區(qū)翔曲,從功能上看迫像,metaspace可以認為和永久區(qū)類似,其最主要的功用也是存放類元數(shù)據(jù)瞳遍,但實際的機制則有較大的不同闻妓。
- 對于JVM里面的內(nèi)存需要在啟動時進行限制,包括我們熟悉的堆內(nèi)存掠械,也要包括直接內(nèi)存和元生區(qū)由缆,這是保證線上服務(wù)正常運行最后的兜底。
- 使用類庫猾蒂,請多注意代碼的寫法均唉,盡量不要出現(xiàn)明顯的內(nèi)存泄漏。
- 對于使用了ASM等字節(jié)碼增強工具的類庫肚菠,在使用他們時請多加小心(尤其是JDK1.8以后)舔箭。
文章撰寫不易,請大家多多支持我的原創(chuàng)技術(shù)公眾號:后端技術(shù)漫談
參考
觀察程序運行時類加載的過程
blog.csdn.net/tenderhearted/article/details/39642275
Metaspace整體介紹(永久代被替換原因案糙、元空間特點限嫌、元空間內(nèi)存查看分析方法)
https://www.cnblogs.com/duanxz/p/3520829.html
java內(nèi)存占用異常問題常見排查流程(含堆外內(nèi)存異常)
https://my.oschina.net/haitaohu/blog/3024843
JVM源碼分析之堆外內(nèi)存完全解讀
http://lovestblog.cn/blog/2015/05/12/direct-buffer/
JVM 類的卸載
https://www.cnblogs.com/caoxb/p/12735525.html
fastjson在jdk1.8上面開啟asm
https://github.com/alibaba/fastjson/issues/385
fastjson:PropertyNamingStrategy_cn
https://github.com/alibaba/fastjson/wiki/PropertyNamingStrategy_cn
警惕動態(tài)代理導(dǎo)致的Metaspace內(nèi)存泄漏問題
https://blog.csdn.net/xyghehehehe/article/details/78820135
關(guān)注我
我是一名后端開發(fā)工程師靴庆。主要關(guān)注后端開發(fā)时捌,數(shù)據(jù)安全,爬蟲炉抒,物聯(lián)網(wǎng)奢讨,邊緣計算等方向,歡迎交流焰薄。
各大平臺都可以找到我
- 微信公眾號:后端技術(shù)漫談
- Github:@qqxx6661
- CSDN:@蠻三刀把刀
- 知乎:@后端技術(shù)漫談
- 簡書:@蠻三刀把刀
- 掘金:@蠻三刀把刀
- 騰訊云+社區(qū):@后端技術(shù)漫談
原創(chuàng)文章主要內(nèi)容
- 后端開發(fā)實戰(zhàn)
- Java面試知識
- 設(shè)計模式/數(shù)據(jù)結(jié)構(gòu)/算法題解
- 讀書筆記/逸聞趣事/程序人生
個人公眾號:后端技術(shù)漫談
如果文章對你有幫助拿诸,不妨點贊,收藏起來~