[轉] Java內存溢出(OOM)異常完全指南1

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

1.java.lang.OutOfMemoryError:Java heap space

Java應用程序在啓動時會指定所需要的內存大小,它被分割成兩個不同的區域:Heap space(堆空間)Permgen(永久代)


JVM內存模型示意圖


這兩個區域的大小可以在JVM(Java虛擬機)啓動時通過參數-Xmx-XX:MaxPermSize設置,如果你沒有顯式設置,則將使用特定平臺的默認值。

當應用程序試圖向堆空間添加更多的數據,但堆卻沒有足夠的空間來容納這些數據時,將會觸發java.lang.OutOfMemoryError: Java heap space異常。需要注意的是:即使有足夠的物理內存可用,只要達到堆空間設置的大小限制,此異常仍然會被觸發。

原因分析

觸發java.lang.OutOfMemoryError: Java heap space最常見的原因就是應用程序需要的堆空間是XXL號的,但是JVM提供的卻是S號。解決方法也很簡單,提供更大的堆空間即可。除了前面的因素還有更復雜的成因:

  • 流量/數據量峯值:應用程序在設計之初均有用戶量和數據量的限制,某一時刻,當用戶數量或數據量突然達到一個峯值,並且這個峯值已經超過了設計之初預期的閾值,那麼以前正常的功能將會停止,並觸發java.lang.OutOfMemoryError: Java heap space異常。

  • 內存泄漏:特定的編程錯誤會導致你的應用程序不停的消耗更多的內存,每次使用有內存泄漏風險的功能就會留下一些不能被回收的對象到堆空間中,隨着時間的推移,泄漏的對象會消耗所有的堆空間,最終觸發java.lang.OutOfMemoryError: Java heap space錯誤。

示例

①、簡單示例

首先看一個非常簡單的示例,下面的代碼試圖創建2 x 1024 x 1024個元素的整型數組,當你嘗試編譯並指定12M堆空間運行時(java -Xmx12m OOM)將會失敗並拋出java.lang.OutOfMemoryError: Java heap space錯誤,而當你指定13M堆空間時,將正常的運行。

計算數組佔用內存大小,不再本文的範圍內,讀者有興趣,可以自行計算

class OOM {    static final int SIZE=2*1024*1024;    public static void main(String[] a) {        int[] i = new int[SIZE];    }}

運行如下:

D:\>javac OOM.javaD:\>java -Xmx12m OOMException in thread "main" java.lang.OutOfMemoryError: Java heap space        at OOM.main(OOM.java:4)D:\>java -Xmx13m OOM
②、內存泄漏示例

在Java中,當開發者創建一個新對象(比如:new Integer(5))時,不需要自己開闢內存空間,而是把它交給JVM。在應用程序整個生命週期類,JVM負責檢查哪些對象可用,哪些對象未被使用。未使用對象將被丟棄,其佔用的內存也將被回收,這一過程被稱爲垃圾回收。JVM負責垃圾回收的模塊集合被稱爲垃圾回收器(GC)。

Java的內存自動管理機制依賴於GC定期查找未使用對象並刪除它們。Java中的內存泄漏是由於GC無法識別一些已經不再使用的對象,而這些未使用的對象一直留在堆空間中,這種堆積最終會導致java.lang.OutOfMemoryError: Java heap space錯誤。

我們可以非常容易的寫出導致內存泄漏的Java代碼:

public class KeylessEntry {        static class Key {        Integer id;                Key(Integer id) {            this.id = id;        }                @Override        public int hashCode() {            return id.hashCode();        }    }    public static void main(String[] args) {        Map<Key,String> m = new HashMap<Key,String>();        while(true) {            for(int i=0;i<10000;i++) {                if(!m.containsKey(new Key(i))) {                    m.put(new Key(i), "Number:" + i);                }            }        }    }}

代碼中HashMap爲本地緩存,第一次while循環,會將10000個元素添加到緩存中。後面的while循環中,由於key已經存在於緩存中,緩存的大小將一直會維持在10000。但事實真的如此嗎?由於Key實體沒有實現equals()方法,導致for循環中每次執行m.containsKey(new Key(i))結果均爲false,其結果就是HashMap中的元素將一直增加。

隨着時間的推移,越來越多的Key對象進入堆空間且不能被垃圾收集器回收(m爲局部變量,GC會認爲這些對象一直可用,所以不會回收),直到所有的堆空間被佔用,最後拋出java.lang.OutOfMemoryError:Java heap space

上面的代碼直接運行可能很久也不會拋出異常,可以在啓動時使用-Xmx參數,設置堆內存大小,或者在for循環後打印HashMap的大小,執行後會發現HashMap的size一直再增長。

解決方法也非常簡單,只要Key實現自己的equals方法即可:

Overridepublic boolean equals(Object o) {    boolean response = false;    if (o instanceof Key) {        response = (((Key)o).id).equals(this.id);    }    return response;}

解決方案

第一個解決方案是顯而易見的,你應該確保有足夠的堆空間來正常運行你的應用程序,在JVM的啓動配置中增加如下配置:

-Xmx1024m

上面的配置分配1024M堆空間給你的應用程序,當然你也可以使用其他單位,比如用G表示GB,K表示KB。下面的示例都表示最大堆空間爲1GB:

java -Xmx1073741824 com.mycompany.MyClassjava -Xmx1048576k com.mycompany.MyClassjava -Xmx1024m com.mycompany.MyClassjava -Xmx1g com.mycompany.MyClass

然後,更多的時候,單純地增加堆空間不能解決所有的問題。如果你的程序存在內存泄漏,一味的增加堆空間也只是推遲java.lang.OutOfMemoryError: Java heap space錯誤出現的時間而已,並未解決這個隱患。除此之外,垃圾收集器在GC時,應用程序會停止運行直到GC完成,而增加堆空間也會導致GC時間延長,進而影響程序的吞吐量。

如果你想完全解決這個問題,那就好好提升自己的編程技能吧,當然運用好Debuggers, profilers, heap dump analyzers等工具,可以讓你的程序最大程度的避免內存泄漏問題。

2、java.lang.OutOfMemoryError:GC overhead limit exceeded

Java運行時環境(JRE)包含一個內置的垃圾回收進程,而在許多其他的編程語言中,開發者需要手動分配和釋放內存。

Java應用程序只需要開發者分配內存,每當在內存中特定的空間不再使用時,一個單獨的垃圾收集進程會清空這些內存空間。垃圾收集器怎樣檢測內存中的某些空間不再使用已經超出本文的範圍,但你只需要相信GC可以做好這些工作即可。

默認情況下,當應用程序花費超過98%的時間用來做GC並且回收了不到2%的堆內存時,會拋出java.lang.OutOfMemoryError:GC overhead limit exceeded錯誤。具體的表現就是你的應用幾乎耗盡所有可用內存,並且GC多次均未能清理乾淨。

原因分析

java.lang.OutOfMemoryError:GC overhead limit exceeded錯誤是一個信號,示意你的應用程序在垃圾收集上花費了太多時間但卻沒有什麼卵用。默認超過98%的時間用來做GC卻回收了不到2%的內存時將會拋出此錯誤。那如果沒有此限制會發生什麼呢?GC進程將被重啓,100%的CPU將用於GC,而沒有CPU資源用於其他正常的工作。如果一個工作本來只需要幾毫秒即可完成,現在卻需要幾分鐘才能完成,我想這種結果誰都沒有辦法接受。

所以java.lang.OutOfMemoryError:GC overhead limit exceeded也可以看做是一個fail-fast(快速失敗)實戰的實例。

示例

下面的代碼初始化一個map並在無限循環中不停的添加鍵值對,運行後將會拋出GC overhead limit exceeded錯誤:

public class Wrapper {    public static void main(String args[]) throws Exception {        Map map = System.getProperties();        Random r = new Random();        while (true) {            map.put(r.nextInt(), "value");        }    }}

正如你所預料的那樣,程序不能正常的結束,事實上,當我們使用如下參數啓動程序時:

java -Xmx100m -XX:+UseParallelGC Wrapper

我們很快就可以看到程序拋出java.lang.OutOfMemoryError: GC overhead limit exceeded錯誤。但如果在啓動時設置不同的堆空間大小或者使用不同的GC算法,比如這樣:

java -Xmx10m -XX:+UseParallelGC Wrapper

我們將看到如下錯誤:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space    at java.util.Hashtable.rehash(Unknown Source)    at java.util.Hashtable.addEntry(Unknown Source)    at java.util.Hashtable.put(Unknown Source)    at cn.moondev.Wrapper.main(Wrapper.java:12)

使用以下GC算法:-XX:+UseConcMarkSweepGC 或者-XX:+UseG1GC,啓動命令如下:

java -Xmx100m -XX:+UseConcMarkSweepGC Wrapperjava -Xmx100m -XX:+UseG1GC Wrapper

得到的結果是這樣的:

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

錯誤已經被默認的異常處理程序捕獲,並且沒有任何錯誤的堆棧信息輸出。

以上這些變化可以說明,在資源有限的情況下,你根本無法無法預測你的應用是怎樣掛掉的,什麼時候會掛掉,所以在開發時,你不能僅僅保證自己的應用程序在特定的環境下正常運行。

解決方案

首先是一個毫無誠意的解決方案,如果你僅僅是不想看到java.lang.OutOfMemoryError:GC overhead limit exceeded的錯誤信息,可以在應用程序啓動時添加如下JVM參數:

-XX:-UseGCOverheadLimit

但是強烈建議不要使用這個選項,因爲這樣並沒有解決任何問題,只是推遲了錯誤出現的時間,錯誤信息也變成了我們更熟悉的java.lang.OutOfMemoryError: Java heap space而已。

另一個解決方案,如果你的應用程序確實內存不足,增加堆內存會解決GC overhead limit問題,就如下面這樣,給你的應用程序1G的堆內存:

java -Xmx1024m com.yourcompany.YourClass

但如果你想確保你已經解決了潛在的問題,而不是掩蓋java.lang.OutOfMemoryError: GC overhead limit exceeded錯誤,那麼你不應該僅止步於此。你要記得還有profilersmemory dump analyzers這些工具,你需要花費更多的時間和精力來查找問題。還有一點需要注意,這些工具在Java運行時有顯著的開銷,因此不建議在生產環境中使用。

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