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中方式:
- 啟動時添加 JVM參數
-XX:+HeapDumpOnOutOfMemoryError參數表示當JVM發生OOM時,自動生成DUMP文件。
- 使用jmap
jmap -dump:live,format=b,file=heap.bin <pid>
- 使用arthas
heapdump
生成堆轉儲文件之后, 需要dump到本地進行分析
分析堆轉儲文件的三種方式:
- jhat
jhat -port 8000 java_pid2162.hprof
jhat默認端口是7000, 如果有端口占用的情況, 可以通過 -port 參數替換默認端口
- visualVm
JVisualVm
- 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 的兩個調整思路:
- 嘗試調整新生代和老年代的比例, 將新生代的比例調大,這樣做的原因在於動態對象年齡判定的機制(同年齡的對象的大小超過整個Survivor區的一半,大於等於這個年齡的對象都會被放入老年代)
- 嘗試更換垃圾回收器(例如將cms更換為 g1)
總結
以上就是我個人的一些分析解決OOM的一些經驗之談, 如果應用發生了OOM的異常, 我們可以通過以下幾個步驟嘗試分析解決:
- 查看日志, 可以定位到對應的代碼段, 然后進行分析是否是應用有問題, 有的話進行修改
- 通過jmap命令查看內存中的對象是什么占用的比較多,是否有需要優化的對象
- 添加對應的jvm參數可以在發生oom的時候生成堆轉儲文件, 然后使用對應的工具或命令來進行分析, 這樣做的好處在於就算應用重啟了依然有跡可循,然后解決問題
- 使用arthas進行分析. arthas不得不說非常的強大, 線上問題排查的利器. 誰用誰知道.
- 使用jstat分析gc的情況和耗時,如果有頻繁的full gc,也許要進行解決
