java.lang.OutOfMemoryError: Java heap space 分析思路


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官方文檔


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM