Java開發(fā)中的OOM內(nèi)存溢出問題詳解
一、OOM 簡介
1、什么是 OOM ?
OOM,全稱 Out Of Memory,意思是內(nèi)存耗盡或內(nèi)存溢出。
對應(yīng)Java 程序拋出的錯為 java.lang.OutOfMemoryError
,這個錯誤在官方的解釋如下:
Thrown when the Java Virtual Machine cannot allocate an object because it is out of memory, and no more memory could be made available by the garbage collector.
意思就是說,當(dāng) JVM 因為沒有足夠的內(nèi)存來為對象分配空間并且垃圾回收器也已經(jīng)沒有空間可回收時,就會拋出這個 error(注意:這錯誤并非 exception,因為這個問題已經(jīng)嚴重到不足以被應(yīng)用處理)。
二、OOM 原因分析
1、發(fā)生 OOM 的原因
出現(xiàn)了 OOM 就表示內(nèi)存耗盡了,出現(xiàn)這種情況主要原因為:
- 內(nèi)存分配不足:分配給 JVM 虛擬機的內(nèi)存過少(這是在啟動時設(shè)置JVM參數(shù)來指定);
- 應(yīng)用程序問題:應(yīng)用使用內(nèi)存過多,并且用完后沒有及時釋放造成浪費,此時就會造成內(nèi)存泄露或者內(nèi)存溢出。
內(nèi)存泄露與溢出:
- 內(nèi)存泄露:申請使用完的內(nèi)存沒有釋放,導(dǎo)致虛擬機不能再次使用該內(nèi)存,此時就造成了內(nèi)存泄露了;
- 內(nèi)存溢出:申請的內(nèi)存超出了 JVM 能提供的內(nèi)存大小,此時稱之為溢出。
2、OOM 的類型
在講解OOM類型時,我們需要了解一下 JAVA 虛擬機的內(nèi)存區(qū)域:
- 程序計數(shù)器:當(dāng)前線程執(zhí)行的字節(jié)碼的行號指示器,線程私有;
- JAVA虛擬機棧:Java方法執(zhí)行的內(nèi)存模型,每個Java方法的執(zhí)行對應(yīng)著一個棧幀的進棧和出棧的操作。
- 本地方法棧:類似JAVA虛擬機棧 ,但是為
native
方法的運行提供內(nèi)存環(huán)境。 - JAVA堆:對象內(nèi)存分配的地方,內(nèi)存垃圾回收的主要區(qū)域,所有線程共享。可分為新生代,老生代。
- 方法區(qū):用于存儲已經(jīng)被JVM加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。Hotspot 中的永久代。
- 運行時常量池:方法區(qū)的一部分,存儲常量信息,如各種字面量、符號引用等。
- 直接內(nèi)存:并不是JVM運行時數(shù)據(jù)區(qū)的一部分, 可直接訪問的內(nèi)存, 比如NIO會用到這部分。
除了程序計數(shù)器不會拋出OOM外,其他各個內(nèi)存區(qū)域都可能會拋出OOM。
常見的OOM情況有以下三種:
java.lang.OutOfMemoryError: Java heap space
:Java 堆內(nèi)存溢出,此種情況最常見,一般由于內(nèi)存泄露或者堆的大小設(shè)置不當(dāng)引起。對于內(nèi)存泄露,需要通過內(nèi)存監(jiān)控軟件查找程序中的泄露代碼,而堆大小可以通過虛擬機參數(shù)-Xms,-Xmx
等修改。java.lang.OutOfMemoryError: PermGen space
:Java 永久代溢出,即方法區(qū)溢出了,一般出現(xiàn)在大量 Class 或者 jsp 頁面,或者采用 cglib 等反射機制的情況。因為上述情況會產(chǎn)生大量的 Class 信息存儲于方法區(qū)。此種情況可以通過更改方法區(qū)的大小來解決,使用類似-XX:PermSize=64m -XX:MaxPermSize=256m
的形式修改。另外,過多的常量尤其是字符串也會導(dǎo)致方法區(qū)溢出。java.lang.StackOverflowError
:不會拋 OOM error,但也是比較常見的 Java 內(nèi)存溢出。JAVA虛擬機棧溢出,一般是由于程序中存在死循環(huán)或者深度遞歸調(diào)用造成的,棧大小設(shè)置太小也會出現(xiàn)此種溢出。可以通過虛擬機參數(shù) -Xss 來設(shè)置棧的大小。
3、分析 OOM
分析 OOM,我們需要借助 Heap Dump 文件(堆轉(zhuǎn)儲文件),它是一個 Java 進程在某個時間點上的內(nèi)存快照,在觸發(fā)快照的時候會保存 java 對象和類的信息。
要獲得 dump 文件,可以采用如下兩種方式:
- 設(shè)置 JVM 參數(shù)
-XX:+HeapDumpOnOutOfMemoryError
:設(shè)定該參數(shù)后,當(dāng)發(fā)生 OOM 時會自動 dump 出堆信息(需要JDK5以上版本)。 - 使用JDK自帶的jmap命令:
jmap -dump:format=b,file=heap.bin <pid>
,其中 pid 可以通過 jps 命令獲取。
dump 堆內(nèi)存信息后,需要對 dump 文件進行分析,從而找到 OOM 的原因。常用的工具有:
mat(eclipse memory analyzer):基于eclipse RCP的內(nèi)存分析工具。詳細信息參見://www.eclipse.org/mat,推薦使用該工具。
jhat:JDK 自帶的 java heap analyze tool,可以將堆中的對象以 html 的形式顯示出來,包括對象的數(shù)量,大小等等,并支持對象查詢語言O(shè)QL。分析相關(guān)的應(yīng)用后,可以通過 //localhost:7000
來訪問分析結(jié)果。
上面兩種方式推薦使用 mat 而不推薦使用 jhat。因為在實際的排查過程中,一般是先在生產(chǎn)環(huán)境 dump 出文件來,然后拉到自己的開發(fā)機器上分析,所以,不如采用高級的分析工具 mat 來的高效。
其他工具:
ARMS (阿里云 APM 產(chǎn)品, 支持 OOM 異常關(guān)鍵字告警):https://help.aliyun.com/document_detail/42966.html;
Alibaba Arthas (阿里 Java 在線診斷工具 Arthas):https://github.com/alibaba/arth
三、OOM 解決方案
1、 Java heap space
當(dāng)堆內(nèi)存 (Heap Space) 沒有足夠空間存放新創(chuàng)建的對象時, 就會拋出 java.lang.OutOfMemoryError:Javaheap space 錯誤,根據(jù)實際生產(chǎn)經(jīng)驗,可以對程序日志中的 OutOfMemoryError 配置關(guān)鍵字告警,一經(jīng)發(fā)現(xiàn),立即處理。
產(chǎn)生的原因:
Java heap space 錯誤產(chǎn)生的常見原因可以分為以下幾類:
- 請求創(chuàng)建一個超大對象,通常是一個大數(shù)組;
- 超出預(yù)期的訪問量或數(shù)據(jù)量,通常是上游系統(tǒng)請求流量飆升,常見于各類促銷或秒殺活動,可以結(jié)合業(yè)務(wù)流量指標(biāo)排查是否有尖狀峰值;
- 過度使用終結(jié)器(Finalizer),該對象沒有立即被 GC;
- 內(nèi)存泄漏(Memory Leak),大量對象引用沒有釋放,JVM 無法對其自動回收,常見于使用了 File 等資源沒有回收。
解決方法:
針對大部分情況,通常只需要通過 -Xmx 參數(shù)調(diào)高 JVM 堆內(nèi)存空間即可。如果仍然沒有解決,可以參考以下情況做進一步處理:
- 如果是超大對象,可以檢查其合理性,比如是否一次性查詢了數(shù)據(jù)庫全部結(jié)果,而沒有做結(jié)果數(shù)限制;
- 如果是業(yè)務(wù)峰值壓力,可以考慮添加機器資源,或者做限流降級;
- 如果是內(nèi)存泄漏,需要找到持有的對象,修改代碼設(shè)計,比如關(guān)閉沒有釋放的連接。
2、 GC overhead limit exceeded
當(dāng) Java 進程花費 98% 以上的時間執(zhí)行 GC,但只恢復(fù)了不到 2% 的內(nèi)存,且該動作連續(xù)重復(fù)了 5 次,就會拋出 java.lang.OutOfMemoryError:GC overhead limit exceeded 錯誤。
簡單地說,就是應(yīng)用程序已經(jīng)基本耗盡了所有可用內(nèi)存,GC 也無法回收。
此類問題的原因與解決方案跟 Java heap space 類似。
3、 Permgen space
該錯誤表示永久代 (Permanent Generation) 已用滿,通常是因為加載的 class 數(shù)目太多或體積太大。
永久代存儲對象主要包括:加載或緩存到內(nèi)存中的 class 定義,包括類的名稱、字段、方法和字節(jié)碼;常量池;對象數(shù)組或類型數(shù)組所關(guān)聯(lián)的 class;JIT 編譯器優(yōu)化后的 class 信息。
PermGen 的使用量與加載到內(nèi)存的 class 的數(shù)量和大小成正相關(guān)。
解決方法:
根據(jù) Permgen space 報錯的時機,可以采用不同的解決方案:
- 程序啟動報錯,修改 -XX:MaxPermSize 啟動參數(shù),調(diào)大永久代空間。
- 應(yīng)用重新部署時報錯,很可能是沒有應(yīng)用沒有重啟,導(dǎo)致加載了多份 class 信息,只需重啟 JVM 即可解決。
- 運行時報錯,應(yīng)用程序可能會動態(tài)創(chuàng)建大量 class,而這些 class 的生命周期很短暫,但是 JVM 默認不會卸載 class,可以設(shè)置 -XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC 這兩個參數(shù)允許 JVM 卸載 class。
如果上述方法無法解決則需要通過 dump 文件逐一分析開銷最大的 classloader 和重復(fù) class。
4、 Metaspace
JDK 1.8 使用 Metaspace 替換了永久代(Permanent Generation),該錯誤表示 Metaspace 已被用滿,通常是因為加載的 class 數(shù)目太多或體積太大。
此類問題的原因與解決方法跟 Permgenspace 非常類似,可以參考上文。需要特別注意的是調(diào)整 Metaspace 空間大小的啟動參數(shù)為 -XX:MaxMetaspaceSize 。
5、 Unable to create new native thread
每個 Java 線程都需要占用一定的內(nèi)存空間,當(dāng) JVM 向底層操作系統(tǒng)請求創(chuàng)建一個新的 native 線程時,如果沒有足夠的資源分配就會報此錯誤。
產(chǎn)生此錯誤的原因有:
- 線程數(shù)超過操作系統(tǒng)最大線程數(shù) ulimit 限制;
- 線程數(shù)超過 kernel.pid_max(只能重啟);
- native 內(nèi)存不足;
解決方法:
- 升級配置,為機器提供更多的內(nèi)存;
- 降低 Java Heap Space 大小;
- 修復(fù)應(yīng)用程序的線程泄漏問題;
- 限制線程池大?。?/li>
- 使用 -Xss 參數(shù)減少線程棧的大??;
- 調(diào)高 OS 層面的線程最大數(shù): 執(zhí)行 ulimia-a 查看最大線程數(shù)限制,使用 ulimit-u xxx 調(diào)整最大線程數(shù)限制。
6、 Out of swap space
該錯誤表示所有可用的虛擬內(nèi)存已被耗盡。虛擬內(nèi)存 (Virtual Memory) 由物理內(nèi)存 (Physical Memory) 和交換空間 (Swap Space) 兩部分組成。
當(dāng)運行時程序請求的虛擬內(nèi)存溢出時就會報 Outof swap space? 錯誤。
常見原因:
- 地址空間不足;
- 物理內(nèi)存已耗光;
- 應(yīng)用程序的本地內(nèi)存泄漏(native leak),例如不斷申請本地內(nèi)存,卻不釋放。
- 執(zhí)行 jmap-histo:live 命令,強制執(zhí)行 Full GC; 如果幾次執(zhí)行后內(nèi)存明顯下降,則基本確認為 Direct ByteBuffer 問題。
解決方法:
- 升級地址空間為 64 bit;
- 使用 Arthas 檢查是否為 Inflater/Deflater 解壓縮問題,如果是,則顯式調(diào)用 end 方法。
- Direct ByteBuffer 問題可以通過啟動參數(shù) -XX:MaxDirectMemorySize 調(diào)低閾值。
- 升級服務(wù)器配置或隔離部署,避免爭用。
7、 Kill process or sacrifice child
有一種內(nèi)核作業(yè) (Kernel Job) 名為 Out of Memory Killer,它會在可用內(nèi)存極低的情況下 殺死(kill)某些進程。
OOM Killer 會對所有進程進行打分,然后將評分較低的進程 殺死,具體的評分規(guī)則可以參考 Surviving the Linux OOM Killer。
不同于其他的 OOM 錯誤,Kill processor sacrifice child 錯誤不是由 JVM 層面觸發(fā)的,而是由操作系統(tǒng)層面觸發(fā)的。默認情況下,Linux 內(nèi)核允許進程申請的內(nèi)存總量大于系統(tǒng)可用內(nèi)存,通過這種 “錯峰復(fù)用” 的方式可以更有效的利用系統(tǒng)資源。
然而,這種方式也會無可避免地帶來一定的 “超賣” 風(fēng)險。例如某些進程持續(xù)占用系統(tǒng)內(nèi)存,然后導(dǎo)致其他進程沒有可用內(nèi)存。此時,系統(tǒng)將自動激活 OOM Killer,尋找評分低的進程,并將其 “殺死”,釋放內(nèi)存資源。
解決方法:
- 升級服務(wù)器配置 / 隔離部署,避免爭用。
- OOM Killer 調(diào)優(yōu)。
8、 Requested array size exceeds VM limit
JVM 限制了數(shù)組的最大長度,該錯誤表示程序請求創(chuàng)建的數(shù)組超過最大長度限制。JVM 在為數(shù)組分配內(nèi)存前,會檢查要分配的數(shù)據(jù)結(jié)構(gòu)在系統(tǒng)中是否可尋址,通常為 Integer.MAX_VALUE - 2。
此類問題比較罕見,通常需要檢查代碼,確認業(yè)務(wù)是否需要創(chuàng)建如此大的數(shù)組,是否可以拆分為多個塊,分批執(zhí)行。
9、 Direct buffer memory
Java 允許應(yīng)用程序通過 Direct ByteBuffer 直接訪問堆外內(nèi)存,許多高性能程序通過 Direct ByteBuffer 結(jié)合內(nèi)存映射文件 (Memory Mapped File) 實現(xiàn)高速 IO。
Direct ByteBuffer 的默認大小為 64 MB,一旦使用超出限制,就會拋出 Directbuffer memory 錯誤。
解決方案:
- Java 只能通過 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通過 Arthas 等在線診斷工具攔截該方法進行排查。
- 檢查是否直接或間接使用了 NIO,如 netty,jetty 等。
- 通過啟動參數(shù) -XX:MaxDirectMemorySize 調(diào)整 Direct ByteBuffer 的上限值。
- 檢查 JVM 參數(shù)是否有 -XX:+DisableExplicitGC 選項,如果有就去掉,因為該參數(shù)會使 System.gc() 失效。
- 檢查堆外內(nèi)存使用代碼,確認是否存在內(nèi)存泄漏; 或者通過反射調(diào)用 sun.misc.Cleaner 的 clean() 方法來主動釋放被 Direct ByteBuffer 持有的內(nèi)存空間。
- 內(nèi)存容量確實不足,升級配置。
到此這篇關(guān)于Java開發(fā)中的OOM內(nèi)存溢出問題詳解的文章就介紹到這了,更多相關(guān)Java中的OOM內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
DoytoQuery中的關(guān)聯(lián)查詢方案示例詳解
這篇文章主要為大家介紹了DoytoQuery中的關(guān)聯(lián)查詢方案示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12JavaEE實現(xiàn)基于SMTP協(xié)議的郵件發(fā)送功能
這篇文章主要為大家詳細介紹了JavaEE實現(xiàn)基于SMTP協(xié)議的郵件發(fā)送功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-05-05JAVA NIO按行讀寫大文件出現(xiàn)中文亂碼問題的解決
這篇文章主要為大家詳細介紹了JAVA在使用NIO進行按行讀寫大文件時出現(xiàn)中文亂碼問題是如何解決的,文中的示例代碼簡潔易懂,有需要的小伙伴可以參考一下2025-02-02Spring之底層架構(gòu)核心概念Environment及用法詳解
這篇文章主要介紹了Spring之底層架構(gòu)核心概念-Environment,本文結(jié)合示例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12java list,set,map,數(shù)組間的相互轉(zhuǎn)換詳解
這篇文章主要介紹了java list,set,map,數(shù)組間的相互轉(zhuǎn)換詳解的相關(guān)資料,這里附有實例代碼,具有參考價值,需要的朋友可以參考下2017-01-01Java?DelayQueue實現(xiàn)延時任務(wù)的示例詳解
DelayQueue是一個無界的BlockingQueue的實現(xiàn)類,用于放置實現(xiàn)了Delayed接口的對象,其中的對象只能在其到期時才能從隊列中取走。本文就來利用DelayQueue實現(xiàn)延時任務(wù),感興趣的可以了解一下2022-08-08java Swing實現(xiàn)選項卡功能(JTabbedPane)實例代碼
這篇文章主要介紹了java Swing實現(xiàn)選項卡功能(JTabbedPane)實例代碼的相關(guān)資料,學(xué)習(xí)java 基礎(chǔ)的朋友可以參考下這個簡單示例,需要的朋友可以參考下2016-11-11