如果線上遇到了OOM,該如何解決?
OOM 意味著程序存在著漏洞,可能是代碼或者 JVM 參數配置引起的。這篇文章和讀者聊聊,Java 進程觸發了 OOM 后如何排查?
常說對生產環境保持敬畏之心,快速解決問題也是一種敬畏的表現。
為什么會 OOM
OOM全稱 “Out Of Memory”,表示內存耗盡。當 JVM 因為沒有足夠的內存來為對象分配空間,并且垃圾回收器也已經沒有空間可回收時,就會拋出這個錯誤。
為什么會出現 OOM,一般由這些問題引起。
- 分配過少:JVM 初始化內存小,業務使用了大量內存;或者不同 JVM 區域分配內存不合理
- 代碼漏洞:某一個對象被頻繁申請,不用了之后卻沒有被釋放,導致內存耗盡
- 內存泄漏:申請使用完的內存沒有釋放,導致虛擬機不能再次使用該內存,此時這段內存就泄露了。因為申請者不用了,而又不能被虛擬機分配給別人用
- 內存溢出:申請的內存超出了 JVM 能提供的內存大小,此時稱之為溢出
內存泄漏持續存在,最后一定會溢出,兩者是因果關系
常見的 OOM
比較常見的 OOM 類型有以下幾種:
java.lang.OutOfMemoryError: PermGen space
Java7 永久代(方法區)溢出,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。每當一個類初次加載的時候,元數據都會存放到永久代
一般出現于大量 Class 對象或者 JSP 頁面,或者采用 CgLib 動態代理技術導致
我們可以通過 -XX:PermSize 和 -XX:MaxPermSize 修改方法區大小
- Java8 將永久代變更為元空間,報錯:java.lang.OutOfMemoryError: Metadata space,元空間內存不足默認進行動態擴展
java.lang.StackOverflowError
虛擬機棧溢出,一般是由于程序中存在 死循環或者深度遞歸調用 造成的。如果棧大小設置過小也會出現溢出,可以通過 -Xss 設置棧的大小
虛擬機拋出棧溢出錯誤,可以在日志中定位到錯誤的類、方法
java.lang.OutOfMemoryError: Java heap space
Java 堆內存溢出,溢出的原因一般由于 JVM 堆內存設置不合理或者內存泄漏導致
如果是內存泄漏,可以通過工具查看泄漏對象到 GC Roots 的引用鏈。掌握了泄漏對象的類型信息以及 GC Roots 引用鏈信息,就可以精準地定位出泄漏代碼的位置
如果不存在內存泄漏,就是內存中的對象確實都還必須存活著,那就應該檢查虛擬機的堆參數(-Xmx 與 -Xms),查看是否可以將虛擬機的內存調大些
小結:方法區和虛擬機棧的溢出場景不在本篇過多討論,下面主要講解常見的 Java 堆空間的 OOM 排查思路
查看 JVM 內存分布
假設我們 Java 應用 PID 為 15162,輸入命令查看 JVM 內存分布 jmap -heap 15162
- [xxx@xxx ~]# jmap -heap 15162
- Attaching to process ID 15162, please wait...
- Debugger attached successfully.
- Server compiler detected.
- JVM version is 25.161-b12
- using thread-local object allocation.
- Mark Sweep Compact GC
- Heap Configuration:
- MinHeapFreeRatio = 40 # 最小堆使用比例
- MaxHeapFreeRatio = 70 # 最大堆可用比例
- MaxHeapSize = 482344960 (460.0MB) # 最大堆空間大小
- NewSize = 10485760 (10.0MB) # 新生代分配大小
- MaxNewSize = 160759808 (153.3125MB) # 最大新生代可分配大小
- OldSize = 20971520 (20.0MB) # 老年代大小
- NewRatio = 2 # 新生代比例
- SurvivorRatio = 8 # 新生代與 Survivor 比例
- MetaspaceSize = 21807104 (20.796875MB) # 元空間大小
- CompressedClassSpaceSize = 1073741824 (1024.0MB) # Compressed Class Space 空間大小限制
- MaxMetaspaceSize = 17592186044415 MB # 最大元空間大小
- G1HeapRegionSize = 0 (0.0MB) # G1 單個 Region 大小
- Heap Usage: # 堆使用情況
- New Generation (Eden + 1 Survivor Space): # 新生代
- capacity = 9502720 (9.0625MB) # 新生代總容量
- used = 4995320 (4.763908386230469MB) # 新生代已使用
- free = 4507400 (4.298591613769531MB) # 新生代剩余容量
- 52.56726495150862% used # 新生代使用占比
- Eden Space:
- capacity = 8454144 (8.0625MB) # Eden 區總容量
- used = 4029752 (3.8430709838867188MB) # Eden 區已使用
- free = 4424392 (4.219429016113281MB) # Eden 區剩余容量
- 47.665996699370154% used # Eden 區使用占比
- From Space: # 其中一個 Survivor 區的內存分布
- capacity = 1048576 (1.0MB)
- used = 965568 (0.92083740234375MB)
- free = 83008 (0.07916259765625MB)
- 92.083740234375% used
- To Space: # 另一個 Survivor 區的內存分布
- capacity = 1048576 (1.0MB)
- used = 0 (0.0MB)
- free = 1048576 (1.0MB)
- 0.0% used
- tenured generation: # 老年代
- capacity = 20971520 (20.0MB)
- used = 10611384 (10.119804382324219MB)
- free = 10360136 (9.880195617675781MB)
- 50.599021911621094% used
- 10730 interned Strings occupying 906232 bytes.
通過查看 JVM 內存分配以及運行時使用情況,可以判斷內存分配是否合理
另外,可以在 JVM 運行時查看最耗費資源的對象,jmap -histo:live 15162 | more
JVM 內存對象列表按照對象所占內存大小排序
- instances:實例數
- bytes:單位 byte
- class name:類名
明顯看到 CustomObjTest 對象實例以及占用內存過多
可惜的是,方案存在局限性,因為它只能排查對象占用內存過高問題
其中 "[" 代表數組,例如 "[C" 代表 Char 數組,"[B" 代表 Byte 數組。如果數組內存占用過多,我們不知道哪些對象持有它,所以就需要 Dump 內存進行離線分析
jmap -histo:live 執行此命令,JVM 會先觸發 GC,再統計信息
Dump 文件分析
Dump 文件是 Java 進程的內存鏡像,其中主要包括 系統信息、虛擬機屬性、完整的線程 Dump、所有類和對象的狀態 等信息
當程序發生內存溢出或 GC 異常情況時,懷疑 JVM 發生了 內存泄漏,這時我們就可以導出 Dump 文件分析
JVM 啟動參數配置添加以下參數
- -XX:+HeapDumpOnOutOfMemoryError
- -XX:HeapDumpPath=./(參數為 Dump 文件生成路徑)
當 JVM 發生 OOM 異常自動導出 Dump 文件,文件名稱默認格式:java_pid{pid}.hprof
上面配置是在應用拋出 OOM 后自動導出 Dump,或者可以在 JVM 運行時導出 Dump 文件
- jmap -dump:file=[文件路徑] [pid]
- # 示例
- jmap -dump:file=./jvmdump.hprof 15162
在本地寫一個測試代碼,驗證下 OOM 以及分析 Dump 文件
- 設置 VM 參數:-Xms3m -Xmx3m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./
- public static void main(String[] args) {
- List<Object> oomList = Lists.newArrayList();
- // 無限循環創建對象
- while (true) {
- oomList.add(new Object());
- }
- }
通過報錯信息得知,java heap space 表示 OOM 發生在堆區,并生成了 hprof 二進制文件在當前文件夾下
JvisualVM 分析
Dump 分析工具有很多,相對而言 JvisualVM、JProfiler、Eclipse Mat,使用人群更多一些。下面以 JvisualVM 舉例分析 Dump 文件
列舉兩個常用的功能,第一個是能看到觸發 OOM 的線程堆棧,清晰得知程序溢出的原因
第二個就是可以查看 JVM 內存里保留大小最大的對象,可以自由選擇排查個數
點擊對象還可以跳轉具體的對象引用詳情頁面
文中 Dump 文件較為簡單,而正式環境出錯的原因五花八門,所以不對該 Dump 文件做深度解析
注意:JvisualVM 如果分析大 Dump 文件,可能會因為內存不足打不開,需要調整默認的內存
總結回顧
線上如遇到 JVM 內存溢出,可以分以下幾步排查
- jmap -heap 查看是否內存分配過小
- jmap -histo 查看是否有明顯的對象分配過多且沒有釋放情況
- jmap -dump 導出 JVM 當前內存快照,使用 JDK 自帶或 MAT 等工具分析快照
如果上面還不能定位問題,那么需要排查應用是否在不斷創建資源,比如網絡連接或者線程,都可能會導致系統資源耗盡
本文轉載自微信公眾號「龍臺的技術筆記」