JVM 問題分析思路

lbr617發表於2022-02-08

1. 前言

工作中有可能遇到 java.lang.OutOfMemoryError: Java heap space 記憶體溢位異常, 本文提供一些記憶體溢位的分析及解決問題的思路.

常見異常如下:

2022-01-31 16:07:29.639 ERROR 1981 --- [http-nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] with root cause

java.lang.OutOfMemoryError: Java heap space

2. 記憶體溢位的問題

解決問題之前先來分析一下為什麼會出現記憶體溢位的問題.

有兩種可能性:

一種是應用有問題, 本該回收的記憶體沒有進行回收導致的記憶體溢位, 這種情況就需要修改程式碼了.

第二種情況則是伺服器資源不夠或JVM引數設定過小導致的記憶體溢位,這種情況需要更換伺服器或修改啟動引數

我們可以使用對應的工具或命令來定位到問題, 然後分析是哪種情況, 最後再解決問題.

3. 場景模擬

通過下列程式碼來模擬記憶體溢位的情況:


// 通過無限建立自定義物件模擬記憶體溢位的場景
@GetMapping("oom")
public void oom(){
    while(true){
        CustomObj customObj = new CustomObj();
    }
}


/**
 * @author liuboren
 * @Title: 自定義物件
 * @Description: 建立該物件用於模擬OOM場景
 * @date 2022/1/30 16:55
 */
public class CustomObj {

// 利用numbers成員變數儘可能更快的用光記憶體
    private int[] numbers = new int[10000000];

}

再將應用的啟動JVM引數設定為 -Xms70m -Xmx70m即可.

通過訪問/oom的介面, 很快程式就會報

2022-01-31 16:07:29.639 ERROR 1981 --- [http-nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space] with root cause

java.lang.OutOfMemoryError: Java heap space

4. 分析的方法

問題已經出來了, 我們可以通過一下幾種方法來定位分析問題:

  • 檢視日誌
  • 使用jmap命令
  • 分析堆轉儲檔案
  • 利用arthas進行分析
  • 使用jstat命令

4.1 日誌分析

通過檢視對應的日誌可以很清晰的定位到錯誤:

java.lang.OutOfMemoryError: Java heap space
	at com.example.demo.entity.CustomObj.<init>(CustomObj.java:11) ~[demo.jar:0.0.1-SNAPSHOT]
	at com.example.demo.controller.TestController.oom(TestController.java:36) ~[demo.jar:0.0.1-SNAPSHOT]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]

可以看到TestController類中的oom方法,裡面的CustomObj物件造成了記憶體溢位.

這時候檢視對應的程式碼進行分析:


@GetMapping("oom")
public void oom(){
    while(true){
        CustomObj customObj = new CustomObj();
    }
}

這個例子是我們使用了while(true) 無限的去創造物件, 所以造成的記憶體溢位, 我們修改對應的程式碼即可.

如果程式正常的情況下,就要考慮修改JVM啟動引數調整堆空間或者將應用放到記憶體更大的伺服器即可.

4.2 jmap

通過日誌只可以定位到對應的程式碼位置,如果我們想看記憶體中到底是什麼物件佔用的空間比較多, 這時候就可以使用jmap命令了

使用下列命令可以檢視記憶體中已產生物件的例項數和大小

jmap -histo pid |head -n 20

-histo引數代表所有的物件,包括已經垃圾回收掉的物件, 如果只想看目前存活的物件可以增加:live引數:

jmap -histo:live pid  |head -n 20

至於head -n 20 則代表輸出排名前20的資料, 如果不加這個引數那麼展示的資料就太多了, 不利於排查問題.

然後看實際效果:

通過上圖可以看出int 型別佔了 40294040bytes 差不多38mb.這是因為我的測試類中的CustomObj物件 new 了一個int陣列導致的.


**
 * @author liuboren
 * @Title: 自定義物件
 * @Description: 建立該物件用於模擬OOM場景
 * @date 2022/1/30 16:55
 */
public class CustomObj {

    private int[] numbers = new int[10000000];

}

使用jmap命令可以快速的檢視記憶體中的物件的例項及佔用的大小, 但是缺點就是顯示的不是那麼直觀, 並且如果應用重啟了那麼也就無法檢視了.

所以為了避免這種情況,可以通過生成堆轉儲檔案來進行分析.

4.3 堆轉儲檔案分析

剛剛說了使用jmap進行記憶體分析的缺點, 現在看看如何使用堆轉儲檔案

生成堆轉儲檔案有3中方式:

  1. 啟動時新增 JVM引數
-XX:+HeapDumpOnOutOfMemoryError參數列示當JVM發生OOM時,自動生成DUMP檔案。
  1. 使用jmap
jmap -dump:live,format=b,file=heap.bin <pid>

  1. 使用arthas
heapdump

生成堆轉儲檔案之後, 需要dump到本地進行分析

分析堆轉儲檔案的三種方式:

  1. jhat
jhat -port 8000 java_pid2162.hprof

jhat預設埠是7000, 如果有埠占用的情況, 可以通過 -port 引數替換預設埠

  1. visualVm
JVisualVm
  1. Eclipse Memory Analyzer

下面看看實際的效果:

  • jhat

利用jhat分析堆轉儲檔案的視覺化效果不是那麼友好, 不重點介紹了, 下圖是可以通過查詢語句來顯示大於50k的物件.

  • VisualVm

執行JVisualVm命令啟動客戶端後, 匯入堆轉儲檔案:

顯示基本的資訊及執行錯誤的執行緒:

點選執行緒可以檢視是執行的哪段程式碼:

物件的型別、例項數及大小

同樣支援利用語句查詢記憶體中的物件, 下面是查詢記憶體中大於5mb的物件

可以看到VisualVm的顯示介面是相當友好的, 並且功能十分的強大,可以檢視是哪個執行緒執行的哪段程式碼,同時也可以檢視物件的型別和大小. 推薦使用VisualVm

  • Eclipse Memory Analyzer

    Eclipse Memory Analyzer 的功能同樣很強大,就是需要額外的裝一些東西, 有興趣的朋友可以參考下面的連結 , 不多做介紹了:
    連結

  • 使用對轉儲檔案的缺點

堆轉儲檔案的優勢是展示介面友好, 並且不會因為應用重啟而丟失, 但是它最大的問題就是, 因為隨著應用的執行對轉儲檔案的體積也在不斷增加, 小則幾g大則幾十上百g. 無論是將檔案dump到本地然還是進行分析都是非常耗時的.

4.4 arthas

Arthas 是Alibaba開源的Java診斷工具. 非常好用, 不瞭解的同學自行百度.

官方文件

下面正文

使用arthas的 jvm和 dashboard命令 可以檢視jvm的情況, 並且使用heapdump也可以生成堆轉儲檔案

jvm命令可以看到 使用的jvm 引數 、使用的垃圾回收器、垃圾回收的時間、新生代老年代的空間、堆記憶體的使用情況等等

啟動引數:

垃圾回收情況:

記憶體使用情況:

dashboard 可以看到執行緒執行情況及記憶體中各個區域的大小及使用情況:

使用heapdump命令可以生成堆轉儲檔案

4.5 jstat

jstat也是jdk自帶的小工具, 功能非常的強大,可以檢視垃圾會回收的次數及時間, 檢視新生代老年代的剩餘空間等等.
命令如下:

jstat -gcutil  pid  1000  

1000是毫秒數,代表每1000毫秒輸出一次

我使用jstat命令主要是檢視應用的full gc的情況, 如果出現頻繁的full gc 這時候就很有必要對程式進行調優了.

頻繁full gc 的兩個調整思路:

  1. 嘗試調整新生代和老年代的比例, 將新生代的比例調大,這樣做的原因在於動態物件年齡判定的機制(同年齡的物件的大小超過整個Survivor區的一半,大於等於這個年齡的物件都會被放入老年代)
  2. 嘗試更換垃圾回收器(例如將cms更換為 g1)

總結

以上就是我個人的一些分析解決OOM的一些經驗之談, 如果應用發生了OOM的異常, 我們可以通過以下幾個步驟嘗試分析解決:

  1. 檢視日誌, 可以定位到對應的程式碼段, 然後進行分析是否是應用有問題, 有的話進行修改
  2. 通過jmap命令檢視記憶體中的物件是什麼佔用的比較多,是否有需要優化的物件
  3. 新增對應的jvm引數可以在發生oom的時候生成堆轉儲檔案, 然後使用對應的工具或命令來進行分析, 這樣做的好處在於就算應用重啟了依然有跡可循,然後解決問題
  4. 使用arthas進行分析. arthas不得不說非常的強大, 線上問題排查的利器. 誰用誰知道.
  5. 使用jstat分析gc的情況和耗時,如果有頻繁的full gc,也許要進行解決

參考連線

jstat命令詳解

堆轉儲檔案分析

arthas官方文件

相關文章