Java Web項目內存溢出問題排查

線上的一個spring boot項目每兩個週會出現系統卡死,不能正常提供api服務,重啓後恢復。經過查看日誌發現大量的“java.lang.OutOfMemoryError: GC overhead limit exceeded”日誌。這個異常的官方解釋:

Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded
Cause: The detail message “GC overhead limit exceeded” indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations.
Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit.

JVM用了98%的時間進行垃圾回收,而只得到2%可用的內存,頻繁的進行內存回收。
結合現象,可以推測程序中某些實例的數量在緩慢的增長,但是一直不能被回收。雖然異常信息不是常見的“java.lang.OutOfMemoryError: Java heap space”,但是原因卻是相同的。

那我就開始查找原因吧!先說一下筆者的思路:

1、從生成獲取dump文件
2、使用jvisualvm.exe或Eclipse Memory Analyzer(MAT)進行內存分析

jvisualvm.exe(JDK自帶的工具)
Eclipse Memory Analyzer(第三方的工具,可不依賴eclipse單獨下載)

3、結合業務代碼進行問題定位
4、提出修改方案

獲取dump文件

可以直接在啓動腳本中添加如下參數,在程序內存溢出時自動生成dump文件

-XX:+HeapDumpOnOutOfMemoryError   # 告訴jvm內存溢出時生成dump文件
-XX:HeapDumpPath=/tmp/dump   # 指定dump的路徑

這個案發現場的第一手資料顯然需要漫長等待過程,不如立即生成一份dump文件
先找到程序的pid(筆者這裏時11008)然後使用下面的命令在當前目錄執行:

/opt/jdk/bin/jmap -dump:format=b,file=20200508.dump 11008

內存分析

如果dump文件比較小,直接使用jdk安裝路徑/bin/jvisualvm.exe來分析,運行jvisualvm.exe,然後點擊File - 裝入
在這裏插入圖片描述
如果文件很大,比如筆者dump文件是2G左右,那就需要使用更強大的MAT工具。

下載地址Eclipse Memory Analyzer(MAT)

打開MAT之前,根據實際dump文件大小,調一下內存大小。
在這裏插入圖片描述
啓動MAT,點擊File - Open Heap Dump

在這裏插入圖片描述
這個工具很快就能加載完成,默認顯示Overview界面
在這裏插入圖片描述
筆者常用的三個功能如上圖標識,先借助Leak Suspects自動分析生成一個報告:
在這裏插入圖片描述
根據自動分析推測com.sun.jmx.mbeanserver.JmxMBeanServer這個對象佔用了已消耗空間的91.93%,但這個類明顯不是我們業務對象。

追根溯源,我們下一步就需要找出這個類和業務代碼的關係。

使用Dominator Tree功能分析對象之間的支配關係。
在這裏插入圖片描述
在支配關係樹種,我們依次展開Retained Heap值最大的那個支配者,最後發現是“org.apache.kafka.common.network.KafkaChannel”持有,最終也沒追溯到我們自己寫的業務代碼。線索斷了。。。。

Retained Heap:表示因爲這個對象,導致所有引用這個對象而不能被回收的內存大小

雖然通過MAT我們沒法定位到業務代碼,但是也提供一個思路:

去業務代碼中review與kafka相關的邏輯。

筆者發現業務代碼中與kafka相關的邏輯,只有使用kafaka-client來連接kafka服務器,而當前這個應用,只是測試連通型,並且有個定時任務每小時會執行一次這個測試。會不會是這個測試代碼寫的有問題,導致對象沒有被釋放,導致的呢?
果然如此,筆者找到的代碼片段如下:

public Integer testConnectionKafka(Long clusterId) {
	// .... 
	// ....
	// ....
	KafkaConsumer<Object, Object> kafkaConsumer = new KafkaConsumer<>(props);
	try {
	    kafkaConsumer.listTopics();
	} catch (Exception ex) {
	    throw new QdamException("KAFKA連接失敗: " + ex.getMessage());
	}
	return SUCCESS;
}

這裏每次調用testConnectionKafka()方法都會創建一個kafkaConsumer, 但是執行完這個方法,並沒有關閉。筆者有理由懷疑可能和這裏有關。修改方案也很簡單,增加kafkaConsumer.close()即可。

} finally {
	kafkaConsumer.close();
}

修改後上線觀察…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章