1. 程式人生 > 實用技巧 >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.Cleanerclean() 方法來主動釋放被 Direct ByteBuffer 持有的記憶體空間。

6、記憶體容量確實不足,升級配置。

本文分享自微信公眾號 -JAVA葵花寶典(Javakhbd),作者:涯海

原文出處及轉載資訊見文內詳細說明,如有侵權,請聯絡[email protected]刪除。

原始發表時間:2019-08-22

本文參與騰訊雲自媒體分享計劃,歡迎正在閱讀的你也加入,一起分享。