Java內存溢出(OOM)異常完全指南3

java.lang.OutOfMemoryError:Unable to create new native thread

一個思考線程的方法是將線程看着是執行任務的工人,如果你只有一個工人,那麼他同時只能執行一項任務,但如果你有十幾個工人,就可以同時完成你幾個任務。就像這些工人都在物理世界,JVM中的線程完成自己的工作也是需要一些空間的,當有足夠多的線程卻沒有那麼多的空間時就會像這樣:


圖片來源:Plumbr

出現java.lang.OutOfMemoryError:Unable to create new native thread就意味着Java應用程序已達到其可以啓動線程數量的極限了。

原因分析

當JVM向OS請求創建一個新線程時,而OS卻無法創建新的native線程時就會拋出Unable to create new native thread錯誤。一臺服務器可以創建的線程數依賴於物理配置和平臺,建議運行下文中的示例代碼來測試找出這些限制。總體上來說,拋出此錯誤會經過以下幾個階段:

  • 運行在JVM內的應用程序請求創建一個新的線程

  • JVM向OS請求創建一個新的native線程

  • OS嘗試創建一個新的native線程,這時需要分配內存給新的線程

  • OS拒絕分配內存給線程,因爲32位Java進程已經耗盡內存地址空間(2-4GB內存地址已被命中)或者OS的虛擬內存已經完全耗盡

  • Unable to create new native thread錯誤將被拋出

示例

下面的示例不能的創建並啓動新的線程。當代碼運行時,很快達到OS的線程數限制,並拋出Unable to create new native thread錯誤。

while(true){    new Thread(new Runnable(){        public void run() {            try {                Thread.sleep(10000000);            } catch(InterruptedException e) { }                }        }).start();}

解決方案

有時,你可以通過在OS級別增加線程數限制來繞過這個錯誤。如果你限制了JVM可在用戶空間創建的線程數,那麼你可以檢查並增加這個限制:

// macOS 10.12上執行$ ulimit -u709

當你的應用程序產生成千上萬的線程,並拋出此異常,表示你的程序已經出現了很嚴重的編程錯誤,我不覺得應該通過修改參數來解決這個問題,不管是OS級別的參數還是JVM啓動參數。更可取的辦法是分析你的應用是否真的需要創建如此多的線程來完成任務?是否可以使用線程池或者說線程池的數量是否合適?是否可以更合理的拆分業務來實現.....

6、java.lang.OutOfMemoryError:Out of swap space?

Java應用程序在啓動時會指定所需要的內存大小,可以通過-Xmx和其他類似的啓動參數來指定。在JVM請求的總內存大於可用物理內存的情況下,操作系統會將內存中的數據交換到磁盤上去。


圖片來源:plumbr

Out of swap space?表示交換空間也將耗盡,並且由於缺少物理內存和交換空間,再次嘗試分配內存也將失敗。

原因分析

當應用程序向JVM native heap請求分配內存失敗並且native heap也即將耗盡時,JVM會拋出Out of swap space錯誤。該錯誤消息中包含分配失敗的大小(以字節爲單位)和請求失敗的原因。

Native Heap Memory是JVM內部使用的Memory,這部分的Memory可以通過JDK提供的JNI的方式去訪問,這部分Memory效率很高,但是管理需要自己去做,如果沒有把握最好不要使用,以防出現內存泄露問題。JVM 使用Native Heap Memory用來優化代碼載入(JTI代碼生成),臨時對象空間申請,以及JVM內部的一些操作。

這個問題往往發生在Java進程已經開始交換的情況下,現代的GC算法已經做得足夠好了,當時當面臨由於交換引起的延遲問題時,GC暫停的時間往往會讓大多數應用程序不能容忍。

java.lang.OutOfMemoryError:Out of swap space?往往是由操作系統級別的問題引起的,例如:

  • 操作系統配置的交換空間不足。

  • 系統上的另一個進程消耗所有內存資源。

還有可能是本地內存泄漏導致應用程序失敗,比如:應用程序調用了native code連續分配內存,但卻沒有被釋放。

解決方案

解決這個問題有幾個辦法,通常最簡單的方法就是增加交換空間,不同平臺實現的方式會有所不同,比如在Linux下可以通過如下命令實現:

# 原作者使用,由於我手裏並沒有Linux環境,所以並未測試# 創建並附加一個大小爲640MB的新交換文件swapoff -a dd if=/dev/zero of=swapfile bs=1024 count=655360mkswap swapfileswapon swapfile

Java GC會掃描內存中的數據,如果是對交換空間運行垃圾回收算法會使GC暫停的時間增加幾個數量級,因此你應該慎重考慮使用上文增加交換空間的方法。

如果你的應用程序部署在JVM需要同其他進程激烈競爭獲取資源的物理機上,建議將服務隔離到單獨的虛擬機中

但在許多情況下,您唯一真正可行的替代方案是:

  • 升級機器以包含更多內存

  • 優化應用程序以減少其內存佔用

當您轉向優化路徑時,使用內存轉儲分析程序來檢測內存中的大分配是一個好的開始。

7、java.lang.OutOfMemoryError:Requested array size exceeds VM limit

Java對應用程序可以分配的最大數組大小有限制。不同平臺限制有所不同,但通常在1到21億個元素之間。


圖片來源:plumbr

當你遇到Requested array size exceeds VM limit錯誤時,意味着你的應用程序試圖分配大於Java虛擬機可以支持的數組。

原因分析

該錯誤由JVM中的native code拋出。 JVM在爲數組分配內存之前,會執行特定於平臺的檢查:分配的數據結構是否在此平臺中是可尋址的。

你很少見到這個錯誤是因爲Java數組的索引是int類型。 Java中的最大正整數爲2 ^ 31 - 1 = 2,147,483,647。 並且平臺特定的限制可以非常接近這個數字,例如:我的環境上(64位macOS,運行Jdk1.8)可以初始化數組的長度高達2,147,483,645(Integer.MAX_VALUE-2)。如果再將數組的長度增加1到Integer.MAX_VALUE-1會導致熟悉的OutOfMemoryError:

Exception in thread "main" java.lang.OutOfMemoryError: Requested array size exceeds VM limit

但是,在使用OpenJDK 6的32位Linux上,在分配具有大約11億個元素的數組時,您將遇到Requested array size exceeded VM limit的錯誤。 要理解你的特定環境的限制,運行下文中描述的小測試程序。

示例

for (int i = 3; i >= 0; i--) {    try {        int[] arr = new int[Integer.MAX_VALUE-i];        System.out.format("Successfully initialized an array with %,d elements.\n", Integer.MAX_VALUE-i);    } catch (Throwable t) {        t.printStackTrace();    }}

該示例重複四次,並在每個回合中初始化一個長原語數組。 該程序嘗試初始化的數組的大小在每次迭代時增加1,最終達到Integer.MAX_VALUE。 現在,當使用Hotspot 7在64位Mac OS X上啓動代碼片段時,應該得到類似於以下內容的輸出:

java.lang.OutOfMemoryError: Java heap space    at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)java.lang.OutOfMemoryError: Java heap space    at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)java.lang.OutOfMemoryError: Requested array size exceeds VM limit    at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)java.lang.OutOfMemoryError: Requested array size exceeds VM limit    at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)

注意,在出現Requested array size exceeded VM limit之前,出現了更熟悉的java.lang.OutOfMemoryError: Java heap space。 這是因爲初始化2 ^ 31-1個元素的數組需要騰出8G的內存空間,大於JVM使用的默認值。

解決方案

java.lang.OutOfMemoryError:Requested array size exceeds VM limit可能會在以下任一情況下出現:

  • 數組增長太大,最終大小在平臺限制和Integer.MAX_INT之間

  • 你有意分配大於2 ^ 31-1個元素的數組

在第一種情況下,檢查你的代碼庫,看看你是否真的需要這麼大的數組。也許你可以減少數組的大小,或者將數組分成更小的數據塊,然後分批處理數據。

在第二種情況下,記住Java數組是由int索引的。因此,當在平臺中使用標準數據結構時,數組不能超過2 ^ 31-1個元素。事實上,在編譯時就會出錯:error:integer number too large

8、Out of memory:Kill process or sacrifice child

爲了理解這個錯誤,我們需要補充一點操作系統的基礎知識。操作系統是建立在進程的概念之上,這些進程在內核中作業,其中有一個非常特殊的進程,名叫“內存殺手(Out of memory killer)”。當內核檢測到系統內存不足時,OOM killer被激活,然後選擇一個進程殺掉。哪一個進程這麼倒黴呢?選擇的算法和想法都很樸實:誰佔用內存最多,誰就被幹掉。如果你對OOM Killer感興趣的話,建議你閱讀參考資料2中的文章。


OOM Killer,圖片來源:plumbr

當可用虛擬虛擬內存(包括交換空間)消耗到讓整個操作系統面臨風險時,就會產生Out of memory:Kill process or sacrifice child錯誤。在這種情況下,OOM Killer會選擇“流氓進程”並殺死它。

原因分析

默認情況下,Linux內核允許進程請求比系統中可用內存更多的內存,但大多數進程實際上並沒有使用完他們所分配的內存。這就跟現實生活中的寬帶運營商類似,他們向所有消費者出售一個100M的帶寬,遠遠超過用戶實際使用的帶寬,一個10G的鏈路可以非常輕鬆的服務100個(10G/100M)用戶,但實際上寬帶運行商往往會把10G鏈路用於服務150人或者更多,以便讓鏈路的利用率更高,畢竟空閒在那兒也沒什麼意義。

Linux內核採用的機制跟寬帶運營商差不多,一般情況下都沒有問題,但當大多數應用程序都消耗完自己的內存時,麻煩就來了,因爲這些應用程序的內存需求加起來超出了物理內存(包括 swap)的容量,內核(OOM killer)必須殺掉一些進程才能騰出空間保障系統正常運行。就如同上面的例子中,如果150人都佔用100M的帶寬,那麼總的帶寬肯定超過了10G這條鏈路能承受的範圍。

示例

當你在Linux上運行如下代碼:

public static void main(String[] args){    List<int[]> l = new java.util.ArrayList();    for (int i = 10000; i < 100000; i++) {        try {            l.add(new int[100000000]);        } catch (Throwable t) {            t.printStackTrace();        }    }}

在Linux的系統日誌中/var/log/kern.log會出現以下日誌:

Jun  4 07:41:59 plumbr kernel: [70667120.897649] Out of memory: Kill process 29957 (java) score 366 or sacrifice childJun  4 07:41:59 plumbr kernel: [70667120.897701] Killed process 29957 (java) total-vm:2532680kB, anon-rss:1416508kB, file-rss:0kB

注意:你可能需要調整交換文件和堆大小,否則你將很快見到熟悉的Java heap space異常。在原作者的測試用例中,使用-Xmx2g指定的2g堆,並具有以下交換配置:

# 注意:原作者使用,由於我手裏並沒有Linux環境,所以並未測試swapoff -a dd if=/dev/zero of=swapfile bs=1024 count=655360mkswap swapfileswapon swapfile

解決方案

解決這個問題最有效也是最直接的方法就是升級內存,其他方法諸如:調整OOM Killer配置、水平擴展應用,將內存的負載分攤到若干小實例上..... 我們不建議的做法是增加交換空間,具體原因已經在前文說過。參考資料②中詳細的介紹了怎樣微調OOM Killer配置以及OOM Killer選擇進程算法的實現,建議你參考閱讀。



如果你想學習Java工程化、高性能及分佈式、高性能、深入淺出。性能調優、Spring,MyBatis,Netty源碼分析和大數據等知識點可以來找我。


而現在我就有一個平臺可以提供給你們學習,讓你在實踐中積累經驗掌握原理。主要方向是JAVA架構師。如果你想拿高薪,想突破瓶頸,想跟別人競爭能取得優勢的,想進BAT但是有擔心面試不過的,可以加我的Java架構進階羣:554355695


注:加羣要求


1、具有2-5工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加。 
2、在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加。 
3、如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的,可以加。 
4、覺得自己很牛B,一般需求都能搞定。但是所學的知識點沒有系統化,很難在技術領域繼續突破的可以加。 
5.阿里Java高級大牛直播講解知識點,分享知識,多年工作經驗的梳理和總結,帶着大家全面、科學地建立自己的技術體系和技術認知! 
6.小號加羣一律不給過,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章