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,也許要進行解決