什么是OOM?
OOM,全稱“Out Of Memory”,翻譯成中文就是“內存用完了”,來源於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.
意思就是說,當JVM因為沒有足夠的內存來為對象分配空間並且垃圾回收器也已經沒有空間可回收時,就會拋出這個error(注:非exception,因為這個問題已經嚴重到不足以被應用處理)。
為什么會OOM?
為什么會沒有內存了呢?原因不外乎有兩點:
1)分配的少了:比如虛擬機本身可使用的內存(一般通過啟動時的VM參數指定)太少。
2)應用用的太多,並且用完沒釋放,浪費了。此時就會造成內存泄露或者內存溢出。
內存泄露:申請使用完的內存沒有釋放,導致虛擬機不能再次使用該內存,此時這段內存就泄露了,因為申請者不用了,而又不能被虛擬機分配給別人用。
內存溢出:申請的內存超出了JVM能提供的內存大小,此時稱之為溢出。
在之前沒有垃圾自動回收的日子里,比如C語言和C++語言,我們必須親自負責內存的申請與釋放操作,如果申請了內存,用完后又忘記了釋放,比如C++中的new了但是沒有delete,那么就可能造成內存泄露。偶爾的內存泄露可能不會造成問題,而大量的內存泄露可能會導致內存溢出。
而在Java語言中,由於存在了垃圾自動回收機制,所以,我們一般不用去主動釋放不用的對象所占的內存,也就是理論上來說,是不會存在“內存泄露”的。但是,如果編碼不當,比如,將某個對象的引用放到了全局的Map中,雖然方法結束了,但是由於垃圾回收器會根據對象的引用情況來回收內存,導致該對象不能被及時的回收。如果該種情況出現次數多了,就會導致內存溢出,比如系統中經常使用的緩存機制。Java中的內存泄露,不同於C++中的忘了delete,往往是邏輯上的原因泄露。
OOM的類型
JVM內存模型:
按照JVM規范,JAVA虛擬機在運行時會管理以下的內存區域:
- 程序計數器:當前線程執行的字節碼的行號指示器,線程私有
- JAVA虛擬機棧:Java方法執行的內存模型,每個Java方法的執行對應着一個棧幀的進棧和出棧的操作。
- 本地方法棧:類似“ JAVA虛擬機棧 ”,但是為native方法的運行提供內存環境。
- JAVA堆:對象內存分配的地方,內存垃圾回收的主要區域,所有線程共享。可分為新生代,老生代。
- 方法區:用於存儲已經被JVM加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。Hotspot中的“永久代”。
- 運行時常量池:方法區的一部分,存儲常量信息,如各種字面量、符號引用等。
- 直接內存:並不是JVM運行時數據區的一部分, 可直接訪問的內存, 比如NIO會用到這部分。
按照JVM規范,除了程序計數器不會拋出OOM外,其他各個內存區域都可能會拋出OOM。
常見的OOM情況當 JVM 內存嚴重不足時,就會拋出 java.lang.OutOfMemoryError 錯誤。本文總結了常見的 OOM 原因及其解決方法,如下圖所示。如有遺漏或錯誤,歡迎補充指正。
1、Java heap space
當堆內存(Heap Space)沒有足夠空間存放新創建的對象時,就會拋出 java.lang.OutOfMemoryError:Javaheap space
錯誤(根據實際生產經驗,可以對程序日志中的 OutOfMemoryError 配置關鍵字告警,一經發現,立即處理)。
原因分析
Javaheap space
錯誤產生的常見原因可以分為以下幾類:1、請求創建一個超大對象,通常是一個大數組。2、超出預期的訪問量/數據量,通常是上游系統請求流量飆升,常見於各類促銷/秒殺活動,可以結合業務流量指標排查是否有尖狀峰值。3、過度使用終結器(Finalizer),該對象沒有立即被 GC。4、內存泄漏(Memory Leak),大量對象引用沒有釋放,JVM 無法對其自動回收,常見於使用了 File 等資源沒有回收。
解決方案
針對大部分情況,通常只需要通過 -Xmx
參數調高 JVM 堆內存空間即可。如果仍然沒有解決,可以參考以下情況做進一步處理:1、如果是超大對象,可以檢查其合理性,比如是否一次性查詢了數據庫全部結果,而沒有做結果數限制。2、如果是業務峰值壓力,可以考慮添加機器資源,或者做限流降級。3、如果是內存泄漏,需要找到持有的對象,修改代碼設計,比如關閉沒有釋放的連接。
2、GC overhead limit exceeded
當 Java 進程花費 98% 以上的時間執行 GC,但只恢復了不到 2% 的內存,且該動作連續重復了 5 次,就會拋出 java.lang.OutOfMemoryError:GC overhead limit exceeded
錯誤。簡單地說,就是應用程序已經基本耗盡了所有可用內存, GC 也無法回收。此類問題的原因與解決方案跟 Javaheap space
非常類似,可以參考上文。
3、Permgen space
該錯誤表示永久代(Permanent Generation)已用滿,通常是因為加載的 class 數目太多或體積太大。
原因分析
永久代存儲對象主要包括以下幾類:1、加載/緩存到內存中的 class 定義,包括類的名稱,字段,方法和字節碼;2、常量池;3、對象數組/類型數組所關聯的 class;4、JIT 編譯器優化后的 class 信息。PermGen 的使用量與加載到內存的 class 的數量/大小正相關。
解決方案
根據 Permgen space 報錯的時機,可以采用不同的解決方案,如下所示:1、程序啟動報錯,修改 -XX:MaxPermSize
啟動參數,調大永久代空間。2、應用重新部署時報錯,很可能是沒有應用沒有重啟,導致加載了多份 class 信息,只需重啟 JVM 即可解決。3、運行時報錯,應用程序可能會動態創建大量 class,而這些 class 的生命周期很短暫,但是 JVM 默認不會卸載 class,可以設置 -XX:+CMSClassUnloadingEnabled
和 -XX:+UseConcMarkSweepGC
這兩個參數允許 JVM 卸載 class。如果上述方法無法解決,可以通過 jmap 命令 dump 內存對象 jmap-dump:format=b,file=dump.hprof<process-id>
,然后利用 Eclipse MAT https://www.eclipse.org/mat 功能逐一分析開銷最大的 classloader 和重復 class。
4、Metaspace
JDK 1.8 使用 Metaspace 替換了永久代(Permanent Generation),該錯誤表示 Metaspace 已被用滿,通常是因為加載的 class 數目太多或體積太大。此類問題的原因與解決方法跟 Permgenspace
非常類似,可以參考上文。需要特別注意的是調整 Metaspace 空間大小的啟動參數為 -XX:MaxMetaspaceSize
。
5、Unable to create new native thread
每個 Java 線程都需要占用一定的內存空間,當 JVM 向底層操作系統請求創建一個新的 native 線程時,如果沒有足夠的資源分配就會報此類錯誤。
原因分析
JVM 向 OS 請求創建 native 線程失敗,就會拋出 Unableto createnewnativethread
,常見的原因包括以下幾類:1、線程數超過操作系統最大線程數 ulimit 限制;2、線程數超過 kernel.pid_max(只能重啟);3、native 內存不足;該問題發生的常見過程主要包括以下幾步:1、JVM 內部的應用程序請求創建一個新的 Java 線程;2、JVM native 方法代理了該次請求,並向操作系統請求創建一個 native 線程;3、操作系統嘗試創建一個新的 native 線程,並為其分配內存;4、如果操作系統的虛擬內存已耗盡,或是受到 32 位進程的地址空間限制,操作系統就會拒絕本次 native 內存分配;5、JVM 將拋出 java.lang.OutOfMemoryError:Unableto createnewnativethread
錯誤。
解決方案
1、升級配置,為機器提供更多的內存;2、降低 Java Heap Space 大小;3、修復應用程序的線程泄漏問題;4、限制線程池大小;5、使用 -Xss 參數減少線程棧的大小;6、調高 OS 層面的線程最大數:執行 ulimia-a
查看最大線程數限制,使用 ulimit-u xxx
調整最大線程數限制。ulimit -a …. 省略部分內容 ….. max user processes (-u) 16384
6、Out of swap space?
該錯誤表示所有可用的虛擬內存已被耗盡。虛擬內存(Virtual Memory)由物理內存(Physical Memory)和交換空間(Swap Space)兩部分組成。當運行時程序請求的虛擬內存溢出時就會報 Outof swap space?
錯誤。
原因分析
該錯誤出現的常見原因包括以下幾類:1、地址空間不足;2、物理內存已耗光;3、應用程序的本地內存泄漏(native leak),例如不斷申請本地內存,卻不釋放。4、執行 jmap-histo:live<pid>
命令,強制執行 Full GC;如果幾次執行后內存明顯下降,則基本確認為 Direct ByteBuffer 問題。
解決方案
根據錯誤原因可以采取如下解決方案:1、升級地址空間為 64 bit;2、使用 Arthas 檢查是否為 Inflater/Deflater 解壓縮問題,如果是,則顯式調用 end 方法。3、Direct ByteBuffer 問題可以通過啟動參數 -XX:MaxDirectMemorySize
調低閾值。4、升級服務器配置/隔離部署,避免爭用。
7、 Kill process or sacrifice child
有一種內核作業(Kernel Job)名為 Out of Memory Killer,它會在可用內存極低的情況下“殺死”(kill)某些進程。OOM Killer 會對所有進程進行打分,然后將評分較低的進程“殺死”,具體的評分規則可以參考 Surviving the Linux OOM Killer。不同於其他的 OOM 錯誤, Killprocessorsacrifice child
錯誤不是由 JVM 層面觸發的,而是由操作系統層面觸發的。
原因分析
默認情況下,Linux 內核允許進程申請的內存總量大於系統可用內存,通過這種“錯峰復用”的方式可以更有效的利用系統資源。然而,這種方式也會無可避免地帶來一定的“超賣”風險。例如某些進程持續占用系統內存,然后導致其他進程沒有可用內存。此時,系統將自動激活 OOM Killer,尋找評分低的進程,並將其“殺死”,釋放內存資源。
解決方案
1、升級服務器配置/隔離部署,避免爭用。2、OOM Killer 調優。
8、Requested array size exceeds VM limit
JVM 限制了數組的最大長度,該錯誤表示程序請求創建的數組超過最大長度限制。JVM 在為數組分配內存前,會檢查要分配的數據結構在系統中是否可尋址,通常為 Integer.MAX_VALUE-2
。此類問題比較罕見,通常需要檢查代碼,確認業務是否需要創建如此大的數組,是否可以拆分為多個塊,分批執行。
9、Direct buffer memory
Java 允許應用程序通過 Direct ByteBuffer 直接訪問堆外內存,許多高性能程序通過 Direct ByteBuffer 結合內存映射文件(Memory Mapped File)實現高速 IO。
原因分析
Direct ByteBuffer 的默認大小為 64 MB,一旦使用超出限制,就會拋出 Directbuffer memory
錯誤。
解決方案
1、Java 只能通過 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通過 Arthas 等在線診斷工具攔截該方法進行排查。
2、檢查是否直接或間接使用了 NIO,如 netty,jetty 等。
3、通過啟動參數 -XX:MaxDirectMemorySize
調整 Direct ByteBuffer 的上限值。
4、檢查 JVM 參數是否有 -XX:+DisableExplicitGC
選項,如果有就去掉,因為該參數會使 System.gc()
失效。
5、檢查堆外內存使用代碼,確認是否存在內存泄漏;或者通過反射調用 sun.misc.Cleaner
的 clean()
方法來主動釋放被 Direct ByteBuffer 持有的內存空間。
6、內存容量確實不足,升級配置。
原文鏈接:
http://www.shujuyr.com/2665.html