記一次dump檔案分析歷程

Hans_Hu發表於2022-03-12

一、背景

今天下午,正酣暢淋漓的搬磚,突然運維同事在群裡通知,核心服務某個節點記憶體異常,服務假死。神經一下子緊張起來,趕緊跑到運維那邊觀察現象。

觀察的結果是服務記憶體溢位,該服務是核心服務,分配了5G記憶體。運維在轉存快照後,立刻重啟服務後正常。在接下來的一段時間裡,另一臺服務節點也發生了同樣的情況。

二、分析過程

這個服務是另外一個同事負責開發的,本著學習的態度,在拿到運維轉存的dump檔案後,就準備嘗試著分析下問題,由於之前沒有類似的經歷,於是先在網上查了下一般怎麼分析類似的問題。

首先嚐試使用MAT(Memory Analyzer)工具進行分析,下載後就準備載入dump檔案,很不幸由於dump檔案過大,載入失敗了,於是調大了記憶體大小,嘗試再次載入,但此時這個檔案不再嘗試重新載入,直接提示載入失敗。

先不糾結工具的問題,然後網上說JDK自帶的jvisualvm也可以用來分析dump檔案, 但也遇到了同樣記憶體不足的問題,再嘗試修改jvisualvm的記憶體限制後, 成功載入了。

看到的介面是這樣的,很明顯看到char[]佔用了近70%的記憶體,接近4G,這太不正常了,點進去看對應的例項(載入的非常慢,需要耐心)。

在例項數介面中看到例項數達到了千萬級,大部分都是一些檔案的路徑字串資訊。在業務中,我們會生成很多臨時檔案,然後這些臨時檔案會刪除,這裡面大部分儲存的是這些臨時檔案路徑。

到這裡導致記憶體洩露的原因似乎找到了,但好像又還不夠,是什麼原因導致這些臨時變數沒有被回收呢。

回到家後,還是想著這個事情,於是又開始研究起來,這個時候想起來可以再用MAT試著分析下,畢竟據說工具很強大。重啟了電腦之後,經過漫長的等待,載入成功了(果然重啟能解決一切問題)。

MAT的介面是這樣的,裡面包含的資訊比較多,對於我這個菜鳥來說,確實一下子不知道看哪裡。
那就一個個慢慢看吧,Histogram裡面的與使用jvisualvm中看到的資訊是相同的。

接下來進入到Dominator Tree檢視, 列出當前存活的物件的記憶體大小,這看起來像是我需要關注的重點。然後查了下這個類 java.io.DeleteOnExitHook 與 記憶體洩露的相關問題。

這個問題在下面兩個連結中給出了說明,大概意思是在刪除檔案使用 File.deleteOnExit() 方法時,並不是立刻刪除檔案,而是將該檔案路徑維護在類DeleteOnExit的一個LinkedHashSet中,最後在JVM關閉的時候,才會去刪除這裡面的檔案,這個方法不能用於長時間執行的服務。
https://stackoverflow.com/questions/40119188/memory-leak-on-deleteonexithook
https://bugs.openjdk.java.net/browse/JDK-6664633

上面的描述,通過原始碼和JDK文件也都得到了證明。

// java.io.File
// Requests that the file or directory denoted by this abstract pathname be deleted when the virtual machine terminates.
public void deleteOnExit() {
	SecurityManager security = System.getSecurityManager();
	if (security != null) {
		security.checkDelete(path);
	}
	if (isInvalid()) {
		return;
	}
	DeleteOnExitHook.add(path);
}

// java.io.DeleteOnExitHook
private static LinkedHashSet<String> files = new LinkedHashSet<>();

static synchronized void add(String file) {
	if(files == null) {
		// DeleteOnExitHook is running. Too late to add a file
		throw new IllegalStateException("Shutdown in progress");
	}

	files.add(file);
}

三、結論

問題定位於File.deleteOnExit()方法的呼叫,導致記憶體洩漏。呼叫該方法只會將需要刪除檔案的路徑,維護在類DeleteOnExit的一個LinkedHashSet中,在JVM關閉時,才會去真正執行刪除檔案操作。這樣導致DeleteOnExitHook這個物件越來越大,最終記憶體溢位。

File.delete()File.deleteOnExit() 的區別:
當呼叫delete()方法時,直接刪除檔案,不管該檔案是否存在,一經呼叫立即執行
當呼叫deleteOnExit()方法時,只是相當於對deleteOnExit()作一個宣告,當程式執行結束,JVM終止時才真正呼叫deleteOnExit()方法實現刪除操作。

我寫了下面這個測試方法,對比 delete()deleteOnExit()的區別,現象會比較明顯。使用deleteOnExit時是在檔案全部建立,JVM關閉的時候,才一個個刪除檔案,delete會立刻刪除檔案。(所以這個方法的使用場景是怎樣的,我就不太清楚了)

public static void loopTest() throws IOException {
	String root = "D:\\C_Temp\\files\\";

	File path = new File(root);
	if (!path.exists()) {
		path.mkdirs();
	}
	int i = 0;
	while (i < 40000) {
		File file = new File(path, "Hello-" + i + ".txt");
		file.createNewFile();
		file.delete();
//            file.deleteOnExit();
		i++;
	}
}

四、收穫

本次排查經歷最大的收穫就是嘗試利用工具分析dump檔案,以前對這種都是望而卻步,感覺很難。但這次帶著問題去分析、思考,這樣下來也不算過於複雜。有些問題不是問題本身難,是自己把它想得很難。

下面是本次的一些思考和踩過的坑,以作備忘。

1. 獲取dump檔案有兩種方法

1)通過 jmap 工具生成可以生成任意Java程式的dump檔案

# 先找到PID
ps -ef | grep java

# jmap 轉存快照
jmap -dump:format=b,file=/opt/dump/test.dump {PID}

2)通過配置JVM啟動引數

#  當程式出現OutofMemory時,將會在相應的目錄下生成一份dump檔案,如果不指定選項HeapDumpPath則在當前目錄下生成dump檔案
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/dumps

2. MAT需要JDK11才能執行

解決辦法是,開啟MAT的安裝目錄,有一個配置檔案MemoryAnalyzer.ini。開啟這個檔案,在檔案中指定JDK版本即可。新增兩行配置:

-vm D:/jdkPath/bin/javaw.exe

**3. 在使用jvisualvm分析大的dump檔案時,堆查器使用的記憶體不足

修改JAVA_HOME/lib/visualvm/etc/visualvm.conf檔案中 visualvm_default_options="-J-client -J-Xms24 -J-Xmx256m",然後重啟jvisualVM即可

4. MAT修改記憶體空間

分析堆轉儲檔案需要消耗很多的堆空間,為了保證分析的效率和效能,在有條件的情況下,建議分配給 MAT 儘可能多的記憶體資源。兩種方式分配記憶體資源給 MAT:
1)修改啟動引數 MemoryAnalyzer.exe -vmargs -Xmx4g
2)編輯檔案 MemoryAnalyzer.ini 新增 -vmargs – Xmx4g

這裡也列一個代辦項

  • 學習MAT工具的使用

參考的一些文章:

相關文章