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