1. 程式人生 > >分享一次解決線上java應用導致JVM記憶體溢位(OOM)的問題

分享一次解決線上java應用導致JVM記憶體溢位(OOM)的問題

某個線上的應用執行幾天後,總是出現卡死甚至出現OOM的情況。
注:文中圖片可能與描述不符,僅作為演示!

通過Linux的top命令檢視cpu佔比

首先通過top命令檢視,發現某個java程式佔用了較高記憶體:

這裡寫圖片描述

JDK的jps命令確定是哪個java程式

然後通過jps -l 與上面的PID列(2848)比較,確定是 picasso-java-v1.jar 這個java程式佔用cpu過高:

這裡寫圖片描述

通過ps 檢視具體哪個JVM執行緒

當時想的是可能應用內某個執行緒導致死迴圈,使用如下命令檢視2848程序的各個執行緒小號cpu時間

//ps -mp [執行緒號] -o THREAD,tid,time
ps -mp 2848 -o THREAD,tid,time

下圖 %CPU列 為 cpu的百分比,TID列執行緒id
這裡寫圖片描述

找到消耗cpu最大的執行緒(當時線上出現時某個執行緒消耗cpu90%多),這裡為了演示,所以取2858這個執行緒

通過jstack檢視java中的具體執行緒棧資訊

然後把上面執行緒id轉化為16進位制,在shell中使用printf "%x\n" tid即可,結果為b2a:

這裡寫圖片描述

然後使用jstack輸出這個執行緒的呼叫棧:

//jstack [程序id] | grep [執行緒的16進位制id] -A行數
jstack 2848 | grep b2a -A30

這裡寫圖片描述
發現為GC執行緒,原來是jvm記憶體回收導致的cpu過高!

通過jstat檢視記憶體回收情況

使用jstat -gcutil 執行緒數 間隔秒數 次數命令檢視:

這裡寫圖片描述

如圖上面的FGC列Full GC次數為幾百,而FGCT的Full GC秒數達到了幾千,通過設定更多的監控次數觀察,每次Full GC過後,O列的老年代還是99%,可見是記憶體不足導致的一直不停Full GC !

重啟程式,使用-Xmx -Xms設定更大堆記憶體

通過重啟程式,-Xmx2048m -Xms2048m 設定了更大的記憶體引數,緩解了問題!

問題重現,尋找其他原因,使用jmap生成堆轉儲檔案

隔了幾天後,問題重現,此時通過jmap 生成了映象

jmap -dump:format=b,file=dumpfile.dat [pid]

生成的檔案也是非常之大,達到2.1Gb!

柳暗花明,使用Eclipse Memory Analyzer分析出原因

把dump檔案下載到本地,同時下載了Eclipse Memory Analyzer對dump檔案進行分析。

在Eclipse Memory Analyzer中生成Leak Suspects報告:

這裡寫圖片描述

發現是 PoolingHttpClientConnectionManager 這個類導致的。再點選上圖中的Details,檢視詳細資訊:

這裡寫圖片描述

這下清晰了,是阿里的oss類庫導致的,結合程式中的如下程式碼:

OSSClient ossClient = new OSSClient("","");
PutObjectResult putObjectResult = ossClient.putObject("", "", "");

這個方法在程式中沒有使用單例模式而且沒有關閉,每呼叫一次就生成了一個PoolingHttpClientConnectionManager,而且是不可回收的。通過原始碼檢視到IdleConnectionReaper.size()這個類會生成PoolingHttpClientConnectionManager的總數量。

驗證猜測

使用 -Xms20m -Xmx20m 執行以下程式,發現size一直變大,最後導致OOM (java.lang.OutOfMemoryError)

for (int i = 0; i < 60000; i++) {
        OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
        ossClient.putObject("<test-bucket>", "test1234" + UUID.randomUUID(), new File("d:/file.txt"));
        System.out.println("size="+IdleConnectionReaper.size());
        Thread.sleep(2);
}

這裡寫圖片描述

檢視api,得知使用shutdown方法即可關閉OSSClient:

ossClient.shutdown();

再執行以下程式,size一直為0,一切正常:

for (int i = 0; i < 60000; i++) {
        OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
        ossClient.putObject("<test-bucket>", "test1234" + UUID.randomUUID(), new File("d:/file.txt"));
        ossClient.shutdown();
        System.out.println("size="+IdleConnectionReaper.size());
        Thread.sleep(2);
}

至此,終於找到了導致cpu過高和OutOfMemoryError的真凶!