Java內(nèi)存溢出(OOM)異常完全指南

轉(zhuǎn)自Java內(nèi)存溢出(OOM)異常完全指南

我的職業(yè)生涯中見過數(shù)以千計的內(nèi)存溢出異常均與下文中的8種情況相關(guān)阐斜。本文分析什么情況會導致這些異常出現(xiàn)缩赛,提供示例代碼的同時為您提供解決指南恳不。
Nikita Salnikov-Tarnovski
Plumbr Co-Founder and VP of Engineering
本文內(nèi)容來源于Plumbr何荚,對原文內(nèi)容有刪減和補充

這也許是目前最為完整的Java OOM異常的解決指南稻爬。

1丑婿、java.lang.OutOfMemoryError:Java heap space

Java應(yīng)用程序在啟動時會指定所需要的內(nèi)存大小口予,它被分割成兩個不同的區(qū)域:Heap space(堆空間)Permgen(永久代)

JVM內(nèi)存模型示意圖

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

當應(yīng)用程序試圖向堆空間添加更多的數(shù)據(jù)煤辨,但堆卻沒有足夠的空間來容納這些數(shù)據(jù)時,將會觸發(fā)java.lang.OutOfMemoryError: Java heap space異常木张。需要注意的是:即使有足夠的物理內(nèi)存可用众辨,只要達到堆空間設(shè)置的大小限制,此異常仍然會被觸發(fā)舷礼。

原因分析

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

  • 流量/數(shù)據(jù)量峰值:應(yīng)用程序在設(shè)計之初均有用戶量和數(shù)據(jù)量的限制,某一時刻育拨,當用戶數(shù)量或數(shù)據(jù)量突然達到一個峰值谨履,并且這個峰值已經(jīng)超過了設(shè)計之初預(yù)期的閾值,那么以前正常的功能將會停止熬丧,并觸發(fā)java.lang.OutOfMemoryError: Java heap space異常笋粟。
  • 內(nèi)存泄漏:特定的編程錯誤會導致你的應(yīng)用程序不停的消耗更多的內(nèi)存,每次使用有內(nèi)存泄漏風險的功能就會留下一些不能被回收的對象到堆空間中,隨著時間的推移害捕,泄漏的對象會消耗所有的堆空間绿淋,最終觸發(fā)java.lang.OutOfMemoryError: Java heap space錯誤。

示例

①尝盼、簡單示例

首先看一個非常簡單的示例吞滞,下面的代碼試圖創(chuàng)建2 x 1024 x 1024個元素的整型數(shù)組,當你嘗試編譯并指定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.java
D:\>java -Xmx12m OOM
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at OOM.main(OOM.java:4)
D:\>java -Xmx13m OOM

②疮跑、內(nèi)存泄漏示例

在Java中组贺,當開發(fā)者創(chuàng)建一個新對象(比如:new Integer(5))時,不需要自己開辟內(nèi)存空間祖娘,而是把它交給JVM失尖。在應(yīng)用程序整個生命周期類,JVM負責檢查哪些對象可用渐苏,哪些對象未被使用掀潮。未使用對象將被丟棄,其占用的內(nèi)存也將被回收琼富,這一過程被稱為垃圾回收仪吧。JVM負責垃圾回收的模塊集合被稱為垃圾回收器(GC)。

Java的內(nèi)存自動管理機制依賴于GC定期查找未使用對象并刪除它們鞠眉。Java中的內(nèi)存泄漏是由于GC無法識別一些已經(jīng)不再使用的對象薯鼠,而這些未使用的對象一直留在堆空間中,這種堆積最終會導致java.lang.OutOfMemoryError: Java heap space錯誤械蹋。

我們可以非常容易的寫出導致內(nèi)存泄漏的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循環(huán),會將10000個元素添加到緩存中哗戈。后面的while循環(huán)中郊艘,由于key已經(jīng)存在于緩存中,緩存的大小將一直會維持在10000。但事實真的如此嗎?由于Key實體沒有實現(xiàn)equals()方法狸棍,導致for循環(huán)中每次執(zhí)行m.containsKey(new Key(i))結(jié)果均為false,其結(jié)果就是HashMap中的元素將一直增加奈附。

隨著時間的推移,越來越多的Key對象進入堆空間且不能被垃圾收集器回收(m為局部變量煮剧,GC會認為這些對象一直可用,所以不會回收),直到所有的堆空間被占用勉盅,最后拋出java.lang.OutOfMemoryError:Java heap space佑颇。

上面的代碼直接運行可能很久也不會拋出異常,可以在啟動時使用-Xmx參數(shù)草娜,設(shè)置堆內(nèi)存大小挑胸,或者在for循環(huán)后打印HashMap的大小,執(zhí)行后會發(fā)現(xiàn)HashMap的size一直再增長宰闰。

解決方法也非常簡單茬贵,只要Key實現(xiàn)自己的equals方法即可:

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

解決方案

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

-Xmx1024m

上面的配置分配1024M堆空間給你的應(yīng)用程序解藻,當然你也可以使用其他單位,比如用G表示GB葡盗,K表示KB螟左。下面的示例都表示最大堆空間為1GB:

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

然后,更多的時候觅够,單純地增加堆空間不能解決所有的問題胶背。如果你的程序存在內(nèi)存泄漏,一味的增加堆空間也只是推遲java.lang.OutOfMemoryError: Java heap space錯誤出現(xiàn)的時間而已喘先,并未解決這個隱患钳吟。除此之外,垃圾收集器在GC時窘拯,應(yīng)用程序會停止運行直到GC完成红且,而增加堆空間也會導致GC時間延長,進而影響程序的吞吐量树枫。

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

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

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

Java應(yīng)用程序只需要開發(fā)者分配內(nèi)存庄呈,每當在內(nèi)存中特定的空間不再使用時蜕煌,一個單獨的垃圾收集進程會清空這些內(nèi)存空間。垃圾收集器怎樣檢測內(nèi)存中的某些空間不再使用已經(jīng)超出本文的范圍诬留,但你只需要相信GC可以做好這些工作即可斜纪。

默認情況下贫母,當應(yīng)用程序花費超過98%的時間用來做GC并且回收了不到2%的堆內(nèi)存時,會拋出java.lang.OutOfMemoryError:GC overhead limit exceeded錯誤盒刚。具體的表現(xiàn)就是你的應(yīng)用幾乎耗盡所有可用內(nèi)存腺劣,并且GC多次均未能清理干凈。

原因分析

java.lang.OutOfMemoryError:GC overhead limit exceeded錯誤是一個信號因块,示意你的應(yīng)用程序在垃圾收集上花費了太多時間但卻沒有什么卵用橘原。默認超過98%的時間用來做GC卻回收了不到2%的內(nèi)存時將會拋出此錯誤。那如果沒有此限制會發(fā)生什么呢涡上?GC進程將被重啟趾断,100%的CPU將用于GC,而沒有CPU資源用于其他正常的工作吩愧。如果一個工作本來只需要幾毫秒即可完成芋酌,現(xiàn)在卻需要幾分鐘才能完成,我想這種結(jié)果誰都沒有辦法接受耻警。

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

示例

下面的代碼初始化一個map并在無限循環(huán)中不停的添加鍵值對甘穿,運行后將會拋出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");
        }
    }
}

正如你所預(yù)料的那樣腮恩,程序不能正常的結(jié)束,事實上温兼,當我們使用如下參數(shù)啟動程序時:

java -Xmx100m -XX:+UseParallelGC Wrapper

我們很快就可以看到程序拋出java.lang.OutOfMemoryError: GC overhead limit exceeded錯誤秸滴。但如果在啟動時設(shè)置不同的堆空間大小或者使用不同的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 Wrapper
java -Xmx100m -XX:+UseG1GC Wrapper

得到的結(jié)果是這樣的:

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

錯誤已經(jīng)被默認的異常處理程序捕獲荡含,并且沒有任何錯誤的堆棧信息輸出。

以上這些變化可以說明届垫,在資源有限的情況下释液,你根本無法無法預(yù)測你的應(yīng)用是怎樣掛掉的,什么時候會掛掉装处,所以在開發(fā)時误债,你不能僅僅保證自己的應(yīng)用程序在特定的環(huán)境下正常運行。

解決方案

首先是一個毫無誠意的解決方案妄迁,如果你僅僅是不想看到java.lang.OutOfMemoryError:GC overhead limit exceeded的錯誤信息寝蹈,可以在應(yīng)用程序啟動時添加如下JVM參數(shù):

-XX:-UseGCOverheadLimit

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

另一個解決方案黔州,如果你的應(yīng)用程序確實內(nèi)存不足耍鬓,增加堆內(nèi)存會解決GC overhead limit問題阔籽,就如下面這樣,給你的應(yīng)用程序1G的堆內(nèi)存:

java -Xmx1024m com.yourcompany.YourClass

但如果你想確保你已經(jīng)解決了潛在的問題牲蜀,而不是掩蓋java.lang.OutOfMemoryError: GC overhead limit exceeded錯誤仿耽,那么你不應(yīng)該僅止步于此。你要記得還有profilersmemory dump analyzers這些工具各薇,你需要花費更多的時間和精力來查找問題。還有一點需要注意君躺,這些工具在Java運行時有顯著的開銷峭判,因此不建議在生產(chǎn)環(huán)境中使用。

3棕叫、java.lang.OutOfMemoryError:Permgen space

Java中堆空間是JVM管理的最大一塊內(nèi)存空間林螃,可以在JVM啟動時指定堆空間的大小,其中堆被劃分成兩個不同的區(qū)域:新生代(Young)和老年代(Tenured)俺泣,新生代又被劃分為3個區(qū)域:Eden疗认、From SurvivorTo Survivor伏钠,如下圖所示横漏。

圖片來源:并發(fā)編程網(wǎng).png

java.lang.OutOfMemoryError: PermGen space錯誤就表明持久代所在區(qū)域的內(nèi)存已被耗盡。

原因分析

要理解java.lang.OutOfMemoryError: PermGen space出現(xiàn)的原因熟掂,首先需要理解Permanent Generation Space的用處是什么缎浇。持久代主要存儲的是每個類的信息,比如:類加載器引用赴肚、運行時常量池(所有常量素跺、字段引用、方法引用誉券、屬性)指厌、字段(Field)數(shù)據(jù)方法(Method)數(shù)據(jù)踊跟、方法代碼踩验、方法字節(jié)碼等等。我們可以推斷出琴锭,PermGen的大小取決于被加載類的數(shù)量以及類的大小晰甚。

因此,我們可以得出出現(xiàn)java.lang.OutOfMemoryError: PermGen space錯誤的原因是:太多的類或者太大的類被加載到permanent generation(持久代)决帖。

示例

①厕九、最簡單的示例

正如前面所描述的,PermGen的使用與加載到JVM類的數(shù)量有密切關(guān)系地回,下面是一個最簡單的示例:

import javassist.ClassPool;
public class MicroGenerator {
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 100_000_000; i++) {
            generate("cn.moondev.User" + i);
        }
    }

    public static Class generate(String name) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        return pool.makeClass(name).toClass();
    }
}

運行時請設(shè)置JVM參數(shù):-XX:MaxPermSize=5m扁远,值越小越好俊鱼。需要注意的是JDK8已經(jīng)完全移除持久代空間,取而代之的是元空間(Metaspace)畅买,所以示例最好的JDK1.7或者1.6下運行并闲。

代碼在運行時不停的生成類并加載到持久代中,直到撐滿持久代內(nèi)存空間谷羞,最后拋出java.lang.OutOfMemoryError:Permgen space帝火。代碼中類的生成使用了javassist庫。

②湃缎、Redeploy-time

更復雜和實際的一個例子就是Redeploy(重新部署犀填,你可以想象一下你開發(fā)時,點擊eclipse的reploy按鈕或者使用idea時按ctrl + F5時的過程)嗓违。在從服務(wù)器卸載應(yīng)用程序時九巡,當前的classloader以及加載的class在沒有實例引用的情況下,持久代的內(nèi)存空間會被GC清理并回收蹂季。如果應(yīng)用中有類的實例對當前的classloader的引用冕广,那么Permgen區(qū)的class將無法被卸載,導致Permgen區(qū)的內(nèi)存一直增加直到出現(xiàn)Permgen space錯誤偿洁。

不幸的是撒汉,許多第三方庫以及糟糕的資源處理方式(比如:線程、JDBC驅(qū)動程序父能、文件系統(tǒng)句柄)使得卸載以前使用的類加載器變成了一件不可能的事神凑。反過來就意味著在每次重新部署過程中,應(yīng)用程序所有的類的先前版本將仍然駐留在Permgen區(qū)中何吝,你的每次部署都將生成幾十甚至幾百M的垃圾溉委。

就以線程和JDBC驅(qū)動來說說。很多人都會使用線程來處理一下周期性或者耗時較長的任務(wù)爱榕,這個時候一定要注意線程的生命周期問題瓣喊,你需要確保線程不能比你的應(yīng)用程序活得還長。否則黔酥,如果應(yīng)用程序已經(jīng)被卸載藻三,線程還在繼續(xù)運行,這個線程通常會維持對應(yīng)用程序的classloader的引用跪者,造成的結(jié)果就不再多說棵帽。多說一句,開發(fā)者有責任處理好這個問題渣玲,特別是如果你是第三方庫的提供者的話逗概,一定要提供線程關(guān)閉接口來處理清理工作

讓我們想象一個使用JDBC驅(qū)動程序連接到關(guān)系數(shù)據(jù)庫的示例應(yīng)用程序忘衍。當應(yīng)用程序部署到服務(wù)器上的時:服務(wù)器創(chuàng)建一個classloader實例來加載應(yīng)用所有的類(包含相應(yīng)的JDBC驅(qū)動)逾苫。根據(jù)JDBC規(guī)范卿城,JDBC驅(qū)動程序(比如:com.mysql.jdbc.Driver)會在初始化時將自己注冊到java.sql.DriverManager中。該注冊過程中會將驅(qū)動程序的一個實例存儲在DriverManager的靜態(tài)字段內(nèi)铅搓,代碼可以參考:

// com.mysql.jdbc.Driver源碼
package com.mysql.jdbc;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can\'t register driver!");
        }
    }
}
// // // // // // // // // //
// 再看下DriverManager對應(yīng)代碼
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

public static synchronized void registerDriver(java.sql.Driver driver,DriverAction da) throws SQLException {
    if(driver != null) {
        registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
    } else {
        throw new NullPointerException();
    }
}

現(xiàn)在瑟押,當從服務(wù)器上卸載應(yīng)用程序的時候,java.sql.DriverManager仍將持有那個驅(qū)動程序的引用星掰,進而持有用于加載應(yīng)用程序的classloader的一個實例的引用多望。這個classloader現(xiàn)在仍然引用著應(yīng)用程序的所有類。如果此程序啟動時需要加載2000個類氢烘,占用約10MB永久代(PermGen)內(nèi)存便斥,那么只需要5~10次重新部署,就會將默認大小的永久代(PermGen)塞滿威始,然后就會觸發(fā)java.lang.OutOfMemoryError: PermGen space錯誤并崩潰。

解決方案

① 解決初始化時的OutOfMemoryError

當在應(yīng)用程序啟動期間觸發(fā)由于PermGen耗盡引起的OutOfMemoryError時像街,解決方案很簡單黎棠。 應(yīng)用程序需要更多的空間來加載所有的類到PermGen區(qū)域,所以我們只需要增加它的大小镰绎。 為此脓斩,請更改應(yīng)用程序啟動配置,并添加(或增加畴栖,如果存在)-XX:MaxPermSize參數(shù)随静,類似于以下示例:

java -XX:MaxPermSize=512m com.yourcompany.YourClass

② 解決Redeploy時的OutOfMemoryError

分析dump文件:首先,找出引用在哪里被持有吗讶;其次燎猛,給你的web應(yīng)用程序添加一個關(guān)閉的hook,或者在應(yīng)用程序卸載后移除引用照皆。你可以使用如下命令導出dump文件:

jmap -dump:format=b,file=dump.hprof <process-id>

如果是你自己代碼的問題請及時修改重绷,如果是第三方庫,請試著搜索一下是否存在"關(guān)閉"接口膜毁,如果沒有給開發(fā)者提交一個bug或者issue吧昭卓。

③ 解決運行時OutOfMemoryError

首先你需要檢查是否允許GC從PermGen卸載類,JVM的標準配置相當保守瘟滨,只要類一創(chuàng)建候醒,即使已經(jīng)沒有實例引用它們,其仍將保留在內(nèi)存中杂瘸,特別是當應(yīng)用程序需要動態(tài)創(chuàng)建大量的類但其生命周期并不長時倒淫,允許JVM卸載類對應(yīng)用大有助益,你可以通過在啟動腳本中添加以下配置參數(shù)來實現(xiàn):

-XX:+CMSClassUnloadingEnabled

默認情況下胧沫,這個配置是未啟用的昌简,如果你啟用它占业,GC將掃描PermGen區(qū)并清理已經(jīng)不再使用的類。但請注意纯赎,這個配置只在UseConcMarkSweepGC的情況下生效谦疾,如果你使用其他GC算法,比如:ParallelGC或者Serial GC時犬金,這個配置無效念恍。所以使用以上配置時,請配合:

-XX:+UseConcMarkSweepGC

如果你已經(jīng)確保JVM可以卸載類晚顷,但是仍然出現(xiàn)內(nèi)存溢出問題峰伙,那么你應(yīng)該繼續(xù)分析dump文件,使用以下命令生成dump文件:

jmap -dump:file=dump.hprof,format=b <process-id>

當你拿到生成的堆轉(zhuǎn)儲文件该默,并利用像Eclipse Memory Analyzer Toolkit這樣的工具來尋找應(yīng)該卸載卻沒被卸載的類加載器瞳氓,然后對該類加載器加載的類進行排查,找到可疑對象栓袖,分析使用或者生成這些類的代碼匣摘,查找產(chǎn)生問題的根源并解決它。

4裹刮、java.lang.OutOfMemoryError:Metaspace

前文已經(jīng)提過音榜,PermGen區(qū)域用于存儲類的名稱和字段,類的方法捧弃,方法的字節(jié)碼赠叼,常量池,JIT優(yōu)化等违霞,但從Java8開始嘴办,Java中的內(nèi)存模型發(fā)生了重大變化:引入了稱為Metaspace的新內(nèi)存區(qū)域,而刪除了PermGen區(qū)域买鸽。請注意:不是簡單的將PermGen區(qū)所存儲的內(nèi)容直接移到Metaspace區(qū)户辞,PermGen區(qū)中的某些部分,已經(jīng)移動到了普通堆里面癞谒。

OOM-example-metaspace底燎,圖片來源:Plumbr

原因分析

Java8做出如此改變的原因包括但不限于:

  • 應(yīng)用程序所需要的PermGen區(qū)大小很難預(yù)測,設(shè)置太小會觸發(fā)PermGen OutOfMemoryError錯誤弹砚,過度設(shè)置導致資源浪費双仍。
  • 提升GC性能,在HotSpot中的每個垃圾收集器需要專門的代碼來處理存儲在PermGen中的類的元數(shù)據(jù)信息桌吃。從PermGen分離類的元數(shù)據(jù)信息到Metaspace朱沃,由于Metaspace的分配具有和Java Heap相同的地址空間,因此MetaspaceJava Heap可以無縫的管理,而且簡化了FullGC的過程逗物,以至將來可以并行的對元數(shù)據(jù)信息進行垃圾收集搬卒,而沒有GC暫停。
  • 支持進一步優(yōu)化翎卓,比如:G1并發(fā)類的卸載契邀,也算為將來做準備吧

正如你所看到的,元空間大小的要求取決于加載的類的數(shù)量以及這種類聲明的大小失暴。 所以很容易看到java.lang.OutOfMemoryError: Metaspace主要原因:太多的類或太大的類加載到元空間坯门。

示例

正如上文中所解釋的,元空間的使用與加載到JVM中的類的數(shù)量密切相關(guān)逗扒。 下面的代碼是最簡單的例子:

public class Metaspace {
    static javassist.ClassPool cp = javassist.ClassPool.getDefault();

    public static void main(String[] args) throws Exception{
        for (int i = 0; ; i++) { 
            Class c = cp.makeClass("eu.plumbr.demo.Generated" + i).toClass();
            System.out.println(i);
        }
    }
}

程序運行中不停的生成新類古戴,所有的這些類的定義將被加載到Metaspace區(qū),直到空間被完全占用并且拋出java.lang.OutOfMemoryError:Metaspace矩肩。當使用-XX:MaxMetaspaceSize = 32m啟動時现恼,大約加載30000多個類時就會死機。

31023
31024
Exception in thread "main" javassist.CannotCompileException: by java.lang.OutOfMemoryError: Metaspace
    at javassist.ClassPool.toClass(ClassPool.java:1170)
    at javassist.ClassPool.toClass(ClassPool.java:1113)
    at javassist.ClassPool.toClass(ClassPool.java:1071)
    at javassist.CtClass.toClass(CtClass.java:1275)
    at cn.moondev.book.Metaspace.main(Metaspace.java:12)
    .....

解決方案

第一個解決方案是顯而易見的黍檩,既然應(yīng)用程序會耗盡內(nèi)存中的Metaspace區(qū)空間述暂,那么應(yīng)該增加其大小,更改啟動配置增加如下參數(shù):

// 告訴JVM:Metaspace允許增長到512建炫,然后才能拋出異常
-XX:MaxMetaspaceSize = 512m

另一個方法就是刪除此參數(shù)來完全解除對Metaspace大小的限制(默認是沒有限制的)。默認情況下疼蛾,對于64位服務(wù)器端JVM肛跌,MetaspaceSize默認大小是21M(初始限制值),一旦達到這個限制值察郁,F(xiàn)ullGC將被觸發(fā)進行類卸載衍慎,并且這個限制值將會被重置彼城,新的限制值依賴于Metaspace的剩余容量滓彰。如果沒有足夠空間被釋放,這個限制值將會上升壶愤,反之亦然麦轰。在技術(shù)上Metaspace的尺寸可以增長到交換空間乔夯,而這個時候本地內(nèi)存分配將會失敗(更具體的分析款侵,可以參考:Java PermGen 去哪里了?)末荐。

你可以通過修改各種啟動參數(shù)來“快速修復”這些內(nèi)存溢出錯誤,但你需要正確區(qū)分你是否只是推遲或者隱藏了java.lang.OutOfMemoryError的癥狀新锈。如果你的應(yīng)用程序確實存在內(nèi)存泄漏或者本來就加載了一些不合理的類甲脏,那么所有這些配置都只是推遲問題出現(xiàn)的時間而已,實際也不會改善任何東西。

5块请、java.lang.OutOfMemoryError:Unable to create new native thread

一個思考線程的方法是將線程看著是執(zhí)行任務(wù)的工人娜氏,如果你只有一個工人,那么他同時只能執(zhí)行一項任務(wù)墩新,但如果你有十幾個工人贸弥,就可以同時完成你幾個任務(wù)。就像這些工人都在物理世界抖棘,JVM中的線程完成自己的工作也是需要一些空間的茂腥,當有足夠多的線程卻沒有那么多的空間時就會像這樣:

image.png

出現(xiàn)java.lang.OutOfMemoryError:Unable to create new native thread就意味著Java應(yīng)用程序已達到其可以啟動線程數(shù)量的極限了。

原因分析

當JVM向OS請求創(chuàng)建一個新線程時切省,而OS卻無法創(chuàng)建新的native線程時就會拋出Unable to create new native thread錯誤最岗。一臺服務(wù)器可以創(chuàng)建的線程數(shù)依賴于物理配置和平臺,建議運行下文中的示例代碼來測試找出這些限制朝捆“愣桑總體上來說,拋出此錯誤會經(jīng)過以下幾個階段:

  • 運行在JVM內(nèi)的應(yīng)用程序請求創(chuàng)建一個新的線程
  • JVM向OS請求創(chuàng)建一個新的native線程
  • OS嘗試創(chuàng)建一個新的native線程芙盘,這時需要分配內(nèi)存給新的線程
  • OS拒絕分配內(nèi)存給線程驯用,因為32位Java進程已經(jīng)耗盡內(nèi)存地址空間(2-4GB內(nèi)存地址已被命中)或者OS的虛擬內(nèi)存已經(jīng)完全耗盡
  • Unable to create new native thread錯誤將被拋出

示例

下面的示例不能的創(chuàng)建并啟動新的線程。當代碼運行時儒老,很快達到OS的線程數(shù)限制蝴乔,并拋出Unable to create new native thread錯誤。

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

解決方案

有時驮樊,你可以通過在OS級別增加線程數(shù)限制來繞過這個錯誤薇正。如果你限制了JVM可在用戶空間創(chuàng)建的線程數(shù),那么你可以檢查并增加這個限制:

// macOS 10.12上執(zhí)行
$ ulimit -u
709

當你的應(yīng)用程序產(chǎn)生成千上萬的線程囚衔,并拋出此異常挖腰,表示你的程序已經(jīng)出現(xiàn)了很嚴重的編程錯誤,我不覺得應(yīng)該通過修改參數(shù)來解決這個問題练湿,不管是OS級別的參數(shù)還是JVM啟動參數(shù)猴仑。更可取的辦法是分析你的應(yīng)用是否真的需要創(chuàng)建如此多的線程來完成任務(wù)?是否可以使用線程池或者說線程池的數(shù)量是否合適肥哎?是否可以更合理的拆分業(yè)務(wù)來實現(xiàn).....

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

Java應(yīng)用程序在啟動時會指定所需要的內(nèi)存大小,可以通過-Xmx和其他類似的啟動參數(shù)來指定篡诽。在JVM請求的總內(nèi)存大于可用物理內(nèi)存的情況下榆苞,操作系統(tǒng)會將內(nèi)存中的數(shù)據(jù)交換到磁盤上去。

圖片來源:plumbr

Out of swap space?表示交換空間也將耗盡霞捡,并且由于缺少物理內(nèi)存和交換空間坐漏,再次嘗試分配內(nèi)存也將失敗。

原因分析

當應(yīng)用程序向JVM native heap請求分配內(nèi)存失敗并且native heap也即將耗盡時,JVM會拋出Out of swap space錯誤赊琳。該錯誤消息中包含分配失敗的大薪重病(以字節(jié)為單位)和請求失敗的原因。

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

這個問題往往發(fā)生在Java進程已經(jīng)開始交換的情況下芝囤,現(xiàn)代的GC算法已經(jīng)做得足夠好了似炎,當時當面臨由于交換引起的延遲問題時,GC暫停的時間往往會讓大多數(shù)應(yīng)用程序不能容忍悯姊。

java.lang.OutOfMemoryError:Out of swap space?往往是由操作系統(tǒng)級別的問題引起的羡藐,例如:

  • 操作系統(tǒng)配置的交換空間不足。
  • 系統(tǒng)上的另一個進程消耗所有內(nèi)存資源悯许。

還有可能是本地內(nèi)存泄漏導致應(yīng)用程序失敗仆嗦,比如:應(yīng)用程序調(diào)用了native code連續(xù)分配內(nèi)存,但卻沒有被釋放先壕。

解決方案

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

# 原作者使用,由于我手里并沒有Linux環(huán)境店印,所以并未測試
# 創(chuàng)建并附加一個大小為640MB的新交換文件
swapoff -a 
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile

Java GC會掃描內(nèi)存中的數(shù)據(jù)冈在,如果是對交換空間運行垃圾回收算法會使GC暫停的時間增加幾個數(shù)量級,因此你應(yīng)該慎重考慮使用上文增加交換空間的方法按摘。

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

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

  • 升級機器以包含更多內(nèi)存
  • 優(yōu)化應(yīng)用程序以減少其內(nèi)存占用

當您轉(zhuǎn)向優(yōu)化路徑時炫贤,使用內(nèi)存轉(zhuǎn)儲分析程序來檢測內(nèi)存中的大分配是一個好的開始溅固。

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

Java對應(yīng)用程序可以分配的最大數(shù)組大小有限制兰珍。不同平臺限制有所不同侍郭,但通常在1到21億個元素之間。

圖片來源:plumbr

當你遇到Requested array size exceeds VM limit錯誤時,意味著你的應(yīng)用程序試圖分配大于Java虛擬機可以支持的數(shù)組亮元。

原因分析

該錯誤由JVM中的native code拋出猛计。 JVM在為數(shù)組分配內(nèi)存之前,會執(zhí)行特定于平臺的檢查:分配的數(shù)據(jù)結(jié)構(gòu)是否在此平臺中是可尋址的爆捞。

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

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

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

示例

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();
    }
}

該示例重復四次店煞,并在每個回合中初始化一個長原語數(shù)組蟹演。 該程序嘗試初始化的數(shù)組的大小在每次迭代時增加1,最終達到Integer.MAX_VALUE顷蟀。 現(xiàn)在酒请,當使用Hotspot 7在64位Mac OS X上啟動代碼片段時,應(yīng)該得到類似于以下內(nèi)容的輸出:

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)

注意鸣个,在出現(xiàn)Requested array size exceeded VM limit之前羞反,出現(xiàn)了更熟悉的java.lang.OutOfMemoryError: Java heap space。 這是因為初始化2 ^ 31-1個元素的數(shù)組需要騰出8G的內(nèi)存空間囤萤,大于JVM使用的默認值昼窗。

解決方案

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

  • 數(shù)組增長太大,最終大小在平臺限制和Integer.MAX_INT之間
  • 你有意分配大于2 ^ 31-1個元素的數(shù)組

在第一種情況下涛舍,檢查你的代碼庫澄惊,看看你是否真的需要這么大的數(shù)組。也許你可以減少數(shù)組的大小富雅,或者將數(shù)組分成更小的數(shù)據(jù)塊掸驱,然后分批處理數(shù)據(jù)。

在第二種情況下没佑,記住Java數(shù)組是由int索引的毕贼。因此,當在平臺中使用標準數(shù)據(jù)結(jié)構(gòu)時蛤奢,數(shù)組不能超過2 ^ 31-1個元素鬼癣。事實上陶贼,在編譯時就會出錯:error:integer number too large

8扣溺、Out of memory:Kill process or sacrifice child

為了理解這個錯誤骇窍,我們需要補充一點操作系統(tǒng)的基礎(chǔ)知識。操作系統(tǒng)是建立在進程的概念之上锥余,這些進程在內(nèi)核中作業(yè)腹纳,其中有一個非常特殊的進程,名叫“內(nèi)存殺手(Out of memory killer)”驱犹。當內(nèi)核檢測到系統(tǒng)內(nèi)存不足時嘲恍,OOM killer被激活,然后選擇一個進程殺掉雄驹。哪一個進程這么倒霉呢佃牛?選擇的算法和想法都很樸實:誰占用內(nèi)存最多,誰就被干掉医舆。如果你對OOM Killer感興趣的話俘侠,建議你閱讀參考資料2中的文章。

OOM Killer蔬将,圖片來源:plumbr

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

原因分析

默認情況下,Linux內(nèi)核允許進程請求比系統(tǒng)中可用內(nèi)存更多的內(nèi)存毙石,但大多數(shù)進程實際上并沒有使用完他們所分配的內(nèi)存廉沮。這就跟現(xiàn)實生活中的寬帶運營商類似,他們向所有消費者出售一個100M的帶寬徐矩,遠遠超過用戶實際使用的帶寬滞时,一個10G的鏈路可以非常輕松的服務(wù)100個(10G/100M)用戶,但實際上寬帶運行商往往會把10G鏈路用于服務(wù)150人或者更多滤灯,以便讓鏈路的利用率更高坪稽,畢竟空閑在那兒也沒什么意義。

Linux內(nèi)核采用的機制跟寬帶運營商差不多力喷,一般情況下都沒有問題刽漂,但當大多數(shù)應(yīng)用程序都消耗完自己的內(nèi)存時演训,麻煩就來了弟孟,因為這些應(yīng)用程序的內(nèi)存需求加起來超出了物理內(nèi)存(包括 swap)的容量,內(nèi)核(OOM killer)必須殺掉一些進程才能騰出空間保障系統(tǒng)正常運行样悟。就如同上面的例子中拂募,如果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的系統(tǒng)日志中/var/log/kern.log會出現(xiàn)以下日志:

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

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

# 注意:原作者使用趴腋,由于我手里并沒有Linux環(huán)境,所以并未測試
swapoff -a 
dd if=/dev/zero of=swapfile bs=1024 count=655360
mkswap swapfile
swapon swapfile

解決方案

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

參考資料:

① 想要了解更多PermGen與Metaspace的內(nèi)容推薦你閱讀:

② 如果你對OOM Killer感興趣的話葵硕,強烈建議你閱讀這篇文章:

備注:水平有限,難免疏漏贯吓,如果問題請留言
本文已經(jīng)同步更新到微信公眾號:輕描淡寫CODE ? Java內(nèi)存溢出(OOM)異常完全指南

轉(zhuǎn)自Java內(nèi)存溢出(OOM)異常完全指南

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末懈凹,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宣决,更是在濱河造成了極大的恐慌蘸劈,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件尊沸,死亡現(xiàn)場離奇詭異威沫,居然都是意外死亡,警方通過查閱死者的電腦和手機洼专,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門棒掠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人屁商,你說我怎么就攤上這事烟很。” “怎么了蜡镶?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵雾袱,是天一觀的道長。 經(jīng)常有香客問我官还,道長芹橡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任望伦,我火速辦了婚禮林说,結(jié)果婚禮上煎殷,老公的妹妹穿的比我還像新娘。我一直安慰自己腿箩,他們只是感情好豪直,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著珠移,像睡著了一般弓乙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钧惧,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天唆貌,我揣著相機與錄音,去河邊找鬼垢乙。 笑死锨咙,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的追逮。 我是一名探鬼主播酪刀,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼钮孵!你這毒婦竟也來了骂倘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤巴席,失蹤者是張志新(化名)和其女友劉穎历涝,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漾唉,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡荧库,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了赵刑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片分衫。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖般此,靈堂內(nèi)的尸體忽然破棺而出蚪战,到底是詐尸還是另有隱情,我是刑警寧澤铐懊,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布邀桑,位于F島的核電站,受9級特大地震影響科乎,放射性物質(zhì)發(fā)生泄漏壁畸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一喜喂、第九天 我趴在偏房一處隱蔽的房頂上張望瓤摧。 院中可真熱鬧,春花似錦玉吁、人聲如沸照弥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽这揣。三九已至,卻和暖如春影斑,著一層夾襖步出監(jiān)牢的瞬間给赞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工矫户, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留片迅,地道東北人。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓皆辽,卻偏偏與公主長得像柑蛇,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子驱闷,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容