前情提要
文檔:【Java內存溢出排查】測試環境服務器掛...
通過以下命令信息可以確定是內存溢出,且Full GC後內存無法得到回收
top -Hp 1 (發現JVM Thread佔用 CPU200%)
jstat -gccause 1 3s (發現大量full gc,且full gc後內存沒有得到回收)
jmap -heap 1 (發現新生代、老年代used佔比均99.99%)
問題分析
新版本代碼部署到預發佈環境後,同樣出現了頻繁Full GC(1w+次),CPU飆高而堆內存回收不了的問題,在代碼發佈上線之前,必須排查出到底原因是什麼?
之前針對測試環境的現場信息分析,基本可以確定不存在比較明顯的內存泄漏(即使存在,也不會是導致服務掛掉的根本原因)
這裏再次用新的方法說明爲什麼得出這個結論——
線索一:MAT(Memery Analysis Tools)指出的唯一內存泄漏可能
根據MAT對我們測試環境怪掉時的堆內存快照分析,給出的唯一的內存泄漏建議如下,換句話說,有一定量ParallelWebappClassLoader對象的引用出現了問題,導致GC無法正常回收這部分內存,那麼一定量指的是多少?12.39%,也就是存在內存泄漏問題的話,對整個堆內存溢出問題的影響是非常小的。一般比較明顯的內存溢出問題,佔比會達到70%以上。
ParallelWebappClassLoader到底是不是內存泄漏?
如果不是ParallelWebappClassLoader對象引用造成的,服務器內存溢出的根因到底又是什麼?
線索二:GC後的內存佔用情況
下面是測試環境服務器正常情況下,執行jmap -histo:live pid | head -n 23 命令的截圖,可以得到當前內存中排名前20的對象實例和class等信息。
這裏說明下jmap -histo:live [pid]命令,執行後,將會觸發一次Full GC,得到的執行結果是Full GC後的內存對象情況。
通過jstat -gccause [pid]命令也可以看到jmap執行後的GC原因,會顯示 “Heap Inspection Initiated GC”
從instances列來看,[C、[I、[B排名靠前是正常的,畢竟是基礎數據類型(char\int\byte)。
當時最先懷疑的是綠色框中的ConcurrentHashMap$Node對象,這可能是某些比較大的map造成的,一般內存泄漏也會是各種map或list持有引用導致的。
所以直接執行下面命令重點觀察ConcurrentHashMap$Node對象的實例數(instances)
jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
得到如下結果,簡單總結一下結果,下面的執行信息太長可以跳過……
1)隨着服務被持續調用,java.util.concurrent.ConcurrentHashMap$Node對象實例數會不斷累加,但GC過後也得到了回收
2)一定時間段內,ConcurrentHashMap$Node對象的回收慢於其增長數量,回收效率比較低
num #instances #bytes class name
----------------------------------------------
1: 516515 77064424 [C
2: 30478 39696288 [B
3: 337998 29743824 java.lang.reflect.Method
4: 16370 18795648 [I
5: 510907 12261768 java.lang.String
6: 374742 11991744 java.util.concurrent.ConcurrentHashMap$Node
7: 222880 10698240 org.aspectj.weaver.reflect.ShadowMatchImpl
8: 130860 7360464 [Ljava.lang.Object;
9: 222880 7132160 org.aspectj.weaver.patterns.ExposedState
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 374823 11994336 java.util.concurrent.ConcurrentHashMap$Node
18: 5441 2834704 [Ljava.util.concurrent.ConcurrentHashMap$Node;
52: 7680 491520 java.util.concurrent.ConcurrentHashMap
561: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
744: 149 2384 java.util.concurrent.ConcurrentHashMap$EntrySetView
927: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1029: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1190: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1719: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3436: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 374851 11995232 java.util.concurrent.ConcurrentHashMap$Node
18: 5455 2835824 [Ljava.util.concurrent.ConcurrentHashMap$Node;
52: 7694 492416 java.util.concurrent.ConcurrentHashMap
564: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
747: 149 2384 java.util.concurrent.ConcurrentHashMap$EntrySetView
930: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1033: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1197: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1719: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3435: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 375425 12013600 java.util.concurrent.ConcurrentHashMap$Node
18: 5742 2858784 [Ljava.util.concurrent.ConcurrentHashMap$Node;
51: 7981 510784 java.util.concurrent.ConcurrentHashMap
581: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
760: 149 2384 java.util.concurrent.ConcurrentHashMap$EntrySetView
939: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1039: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1197: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1719: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3435: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 374815 11994080 java.util.concurrent.ConcurrentHashMap$Node
18: 5437 2834384 [Ljava.util.concurrent.ConcurrentHashMap$Node;
52: 7676 491264 java.util.concurrent.ConcurrentHashMap
561: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
744: 149 2384 java.util.concurrent.ConcurrentHashMap$EntrySetView
926: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1028: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1190: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1719: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3435: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 374850 11995200 java.util.concurrent.ConcurrentHashMap$Node
18: 5450 2835488 [Ljava.util.concurrent.ConcurrentHashMap$Node;
51: 7690 492160 java.util.concurrent.ConcurrentHashMap
563: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
745: 150 2400 java.util.concurrent.ConcurrentHashMap$EntrySetView
928: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1030: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1192: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1718: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3431: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 375404 12012928 java.util.concurrent.ConcurrentHashMap$Node
18: 5697 2855376 [Ljava.util.concurrent.ConcurrentHashMap$Node;
51: 7940 508160 java.util.concurrent.ConcurrentHashMap
576: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
762: 152 2432 java.util.concurrent.ConcurrentHashMap$EntrySetView
943: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1042: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1202: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1732: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3456: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 374882 11996224 java.util.concurrent.ConcurrentHashMap$Node
18: 5449 2835472 [Ljava.util.concurrent.ConcurrentHashMap$Node;
51: 7690 492160 java.util.concurrent.ConcurrentHashMap
559: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
746: 151 2416 java.util.concurrent.ConcurrentHashMap$EntrySetView
928: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1029: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1191: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1718: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3431: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 375174 12005568 java.util.concurrent.ConcurrentHashMap$Node
18: 5462 2838848 [Ljava.util.concurrent.ConcurrentHashMap$Node;
51: 7701 492864 java.util.concurrent.ConcurrentHashMap
560: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
741: 153 2448 java.util.concurrent.ConcurrentHashMap$EntrySetView
926: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1024: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1190: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1714: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3431: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
root@team-app-service-2-599746d64b-h6tzj:/usr/local/tomcat# jmap -histo:live 1 | grep java.util.concurrent.ConcurrentHashMap$Node
6: 376732 12055424 java.util.concurrent.ConcurrentHashMap$Node
18: 5818 2866688 [Ljava.util.concurrent.ConcurrentHashMap$Node;
51: 8078 516992 java.util.concurrent.ConcurrentHashMap
574: 107 5136 java.util.concurrent.ConcurrentHashMap$TreeNode
757: 154 2464 java.util.concurrent.ConcurrentHashMap$EntrySetView
949: 4 1120 java.util.concurrent.ConcurrentHashMap$CounterCell
1051: 34 816 java.util.concurrent.ConcurrentHashMap$KeySetView
1230: 11 528 java.util.concurrent.ConcurrentHashMap$TreeBin
1788: 12 192 java.util.concurrent.ConcurrentHashMap$ValuesView
3509: 2 48 [Ljava.util.concurrent.ConcurrentHashMap$CounterCell;
所以分析ConcurrentHashMap$Node對象可能存在泄漏,在通過MAT查看該對象的引用情況(排除弱引用),根據shallow heap字段倒序排序,得到結果證實了MAT指出的內存泄漏問題對象ParallelWebappClassLoader,說明MAT分析是有道理的,所以可以確定這裏存在比較“輕微”的內存泄漏問題。
爲什麼說是“輕微”的內存泄漏問題?接着就要分析下,服務掛掉的時候,這個ConcurrentHashMap$Node對象佔用堆內存12307392b≈11.73M,而當時老年代大小爲337M,所以不是它壓垮的服務器。
這裏也可以直接看下ParallelWebappClassLoader對象的Retained Heap(深堆內存)大小,這個值意味着如果該對象被回收,將獲得多少的內存收益。
線索三:內存突增
當時只關注了內存泄漏這個點,忘記了流量突增或者是某些特定代碼查詢的數據量突增,也可能導致內存溢出,特別是在老年代設置得比較小的情況下。
而我們測試環境的流量不可能產生突增,基本就是測試同學在進行功能測試,QPS也比較低。從sky walking監控服務掛掉時的請求量也都還算正常
但看內存監控發現有內存的突增,14:53到14:54一分鐘從402M突增到了462M,並且之後還在突增到483M(這時Full GC後老年代used佔比依然是99.99%),測試環境堆的總大小500M,所以服務器內存溢出,應該是這個時間段的某段代碼執行導致的。
問題排查
思路一:排查接口
方向找到了,首先想到是分析關鍵時間段內的接口請求,能夠一下子填滿60+M的數據請求,接口耗時應該會挺長,因爲在內存中遍歷數據需要時間,再加上服務IO也需要時間。
不過由於測試環境日誌被清理了,預發佈環境有沒有監控信息,從接口爲入口的線索斷掉了(況且接口響應慢也有可能是因爲線程阻塞等其他資源競爭,沒有說服力)
思路二:根據內存快照,對比突增內存對應的對象及其引用
沒有接口日誌也沒關係,我對比了一下測試環境,服務器內存溢出時(下圖1)的快照和服務器正常工作時(觀察了比較久,數據穩定)的內存快照(下圖2)
內存增加了47M(圖3),還沒算上其他的一些incoming引用對象的大小,那麼這個突增的大小是對應得上前面分析得問題根因的。
(圖1)
(圖2)
(圖3)
所以可以在服務掛掉時,且執行過GC後的堆內存快照中排查分析“[C”和“[B”這兩塊內存數據,可以更準確的找到這部分出問題的數據,最終找到問題代碼。
先展開char[]對象和其引用,看看具體是什麼東東導致比正常服務時多出來30M數據。
按照Shallow Heap(淺堆空間,即對象實際佔用內存大小)倒敘排序,找一些問題對象看看(哪些是問題對象?1體積比較大的;2體積不大,但個數驚人的)
對比時突然發現當時測試環境怪掉時拿到的堆快照和服務正常時的快照數據幾乎一致,暫時還不知道是不是文件下載錯了,所以目前只能調整測試環境JVM配置,等測試環境再次出現問題時再dump一次堆快照,然後建立可用對照組進一步分析。
再次把服務器搞掛一次之後,拿到監控數據,頻繁Full GC,各堆內存區域used 99%
此時的堆內存對象佔用情況如下,基本和上一次掛掉的數據一致。
MAT建立對照組,分析問題數據,發現問題對象[C 的引用有一個ElasticSearchGroupUnitField,和客源匹配小組日記查詢有關,是ES搜索服務提供的接口。
這一個大char[]對象的實際內存佔用了17141776b ≈ 16.34M,難以想象這個接口發佈到線上,多線程請求執行時服務器能撐多久……
這就是導致測試環境服務器掛掉的根本原因!
問題代碼分析
查詢某個客戶匹配的小組日記列表時,沒有加入小組id條件,而設置的pageSize爲1000條,在沒有指定groupId的情況下,整個小組日記中搜索與之匹配的日記數據,那肯定是每次都是滿載的1000條,而日記的字段又比較多,單個文檔的數據量比較大。
這個業務方法在客源列表、客源詳情、客源匹配日記接口等都有調用,所以將代碼發佈到測試環境後,針對性測試了幾分鐘服務就掛了,問題復現。
修復邏輯比較簡單,就是加入需要查詢的小組id作爲查詢條件,修復後,再次針對以上場景進行測試,GC回收正常,問題解決。
總結
1. 排查服務器內存性能問題應該從2個大方向獲取信息進行分析:
a. 內存泄漏
b. 內存(流量)突增
先確定是什麼病,再對症下藥,以免繞彎子…
2. 加強code review的執行力度,畢竟code review的成本比這樣一頓排查和分析要小得多,反向的排查總是要複雜和困難的
最後,還是感謝 trouble maker,還有幫忙驗證的測試同學!