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

3.java.lang.OutOfMemoryError:Permgen space

Java中堆空間是JVM管理的最大一塊內存空間,可以在JVM啓動時指定堆空間的大小,其中堆被劃分成兩個不同的區域:新生代(Young)和老年代(Tenured),新生代又被劃分爲3個區域:EdenFrom SurvivorTo Survivor,如下圖所示。


圖片來源:併發編程網

java.lang.OutOfMemoryError: PermGen space錯誤就表明持久代所在區域的內存已被耗盡。

原因分析

要理解java.lang.OutOfMemoryError: PermGen space出現的原因,首先需要理解Permanent Generation Space的用處是什麼。持久代主要存儲的是每個類的信息,比如:類加載器引用運行時常量池(所有常量、字段引用、方法引用、屬性)字段(Field)數據方法(Method)數據方法代碼方法字節碼等等。我們可以推斷出,PermGen的大小取決於被加載類的數量以及類的大小。

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

示例

①、最簡單的示例

正如前面所描述的,PermGen的使用與加載到JVM類的數量有密切關係,下面是一個最簡單的示例:

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

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

代碼在運行時不停的生成類並加載到持久代中,直到撐滿持久代內存空間,最後拋出java.lang.OutOfMemoryError:Permgen space。代碼中類的生成使用了javassist庫。

②、Redeploy-time

更復雜和實際的一個例子就是Redeploy(重新部署,你可以想象一下你開發時,點擊eclipse的reploy按鈕或者使用idea時按ctrl + F5時的過程)。在從服務器卸載應用程序時,當前的classloader以及加載的class在沒有實例引用的情況下,持久代的內存空間會被GC清理並回收。如果應用中有類的實例對當前的classloader的引用,那麼Permgen區的class將無法被卸載,導致Permgen區的內存一直增加直到出現Permgen space錯誤。

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

就以線程和JDBC驅動來說說。很多人都會使用線程來處理一下週期性或者耗時較長的任務,這個時候一定要注意線程的生命週期問題,你需要確保線程不能比你的應用程序活得還長。否則,如果應用程序已經被卸載,線程還在繼續運行,這個線程通常會維持對應用程序的classloader的引用,造成的結果就不再多說。多說一句,開發者有責任處理好這個問題,特別是如果你是第三方庫的提供者的話,一定要提供線程關閉接口來處理清理工作

讓我們想象一個使用JDBC驅動程序連接到關係數據庫的示例應用程序。當應用程序部署到服務器上的時:服務器創建一個classloader實例來加載應用所有的類(包含相應的JDBC驅動)。根據JDBC規範,JDBC驅動程序(比如:com.mysql.jdbc.Driver)會在初始化時將自己註冊到java.sql.DriverManager中。該註冊過程中會將驅動程序的一個實例存儲在DriverManager的靜態字段內,代碼可以參考:

// 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對應代碼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();    }}

現在,當從服務器上卸載應用程序的時候,java.sql.DriverManager仍將持有那個驅動程序的引用,進而持有用於加載應用程序的classloader的一個實例的引用。這個classloader現在仍然引用着應用程序的所有類。如果此程序啓動時需要加載2000個類,佔用約10MB永久代(PermGen)內存,那麼只需要5~10次重新部署,就會將默認大小的永久代(PermGen)塞滿,然後就會觸發java.lang.OutOfMemoryError: PermGen space錯誤並崩潰。

解決方案

① 解決初始化時的OutOfMemoryError

當在應用程序啓動期間觸發由於PermGen耗盡引起的OutOfMemoryError時,解決方案很簡單。 應用程序需要更多的空間來加載所有的類到PermGen區域,所以我們只需要增加它的大小。 爲此,請更改應用程序啓動配置,並添加(或增加,如果存在)-XX:MaxPermSize參數,類似於以下示例:

java -XX:MaxPermSize=512m com.yourcompany.YourClass
② 解決Redeploy時的OutOfMemoryError

分析dump文件:首先,找出引用在哪裏被持有;其次,給你的web應用程序添加一個關閉的hook,或者在應用程序卸載後移除引用。你可以使用如下命令導出dump文件:

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

如果是你自己代碼的問題請及時修改,如果是第三方庫,請試着搜索一下是否存在"關閉"接口,如果沒有給開發者提交一個bug或者issue吧。

③ 解決運行時OutOfMemoryError

首先你需要檢查是否允許GC從PermGen卸載類,JVM的標準配置相當保守,只要類一創建,即使已經沒有實例引用它們,其仍將保留在內存中,特別是當應用程序需要動態創建大量的類但其生命週期並不長時,允許JVM卸載類對應用大有助益,你可以通過在啓動腳本中添加以下配置參數來實現:

-XX:+CMSClassUnloadingEnabled

默認情況下,這個配置是未啓用的,如果你啓用它,GC將掃描PermGen區並清理已經不再使用的類。但請注意,這個配置只在UseConcMarkSweepGC的情況下生效,如果你使用其他GC算法,比如:ParallelGC或者Serial GC時,這個配置無效。所以使用以上配置時,請配合:

-XX:+UseConcMarkSweepGC

如果你已經確保JVM可以卸載類,但是仍然出現內存溢出問題,那麼你應該繼續分析dump文件,使用以下命令生成dump文件:

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

當你拿到生成的堆轉儲文件,並利用像Eclipse Memory Analyzer Toolkit這樣的工具來尋找應該卸載卻沒被卸載的類加載器,然後對該類加載器加載的類進行排查,找到可疑對象,分析使用或者生成這些類的代碼,查找產生問題的根源並解決它。

4、java.lang.OutOfMemoryError:Metaspace

前文已經提過,PermGen區域用於存儲類的名稱和字段,類的方法,方法的字節碼,常量池,JIT優化等,但從Java8開始,Java中的內存模型發生了重大變化:引入了稱爲Metaspace的新內存區域,而刪除了PermGen區域。請注意:不是簡單的將PermGen區所存儲的內容直接移到Metaspace區,PermGen區中的某些部分,已經移動到了普通堆裏面。


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

原因分析

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

  • 應用程序所需要的PermGen區大小很難預測,設置太小會觸發PermGen OutOfMemoryError錯誤,過度設置導致資源浪費。

  • 提升GC性能,在HotSpot中的每個垃圾收集器需要專門的代碼來處理存儲在PermGen中的類的元數據信息。從PermGen分離類的元數據信息到Metaspace,由於Metaspace的分配具有和Java Heap相同的地址空間,因此MetaspaceJava Heap可以無縫的管理,而且簡化了FullGC的過程,以至將來可以並行的對元數據信息進行垃圾收集,而沒有GC暫停。

  • 支持進一步優化,比如:G1併發類的卸載,也算爲將來做準備吧

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

示例

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

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

程序運行中不停的生成新類,所有的這些類的定義將被加載到Metaspace區,直到空間被完全佔用並且拋出java.lang.OutOfMemoryError:Metaspace。當使用-XX:MaxMetaspaceSize = 32m啓動時,大約加載30000多個類時就會死機。

3102331024Exception 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)    .....

解決方案

第一個解決方案是顯而易見的,既然應用程序會耗盡內存中的Metaspace區空間,那麼應該增加其大小,更改啓動配置增加如下參數:

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

另一個方法就是刪除此參數來完全解除對Metaspace大小的限制(默認是沒有限制的)。默認情況下,對於64位服務器端JVM,MetaspaceSize默認大小是21M(初始限制值),一旦達到這個限制值,FullGC將被觸發進行類卸載,並且這個限制值將會被重置,新的限制值依賴於Metaspace的剩餘容量。如果沒有足夠空間被釋放,這個限制值將會上升,反之亦然。在技術上Metaspace的尺寸可以增長到交換空間,而這個時候本地內存分配將會失敗(更具體的分析,可以參考:Java PermGen 去哪裏了?)。

你可以通過修改各種啓動參數來“快速修復”這些內存溢出錯誤,但你需要正確區分你是否只是推遲或者隱藏了java.lang.OutOfMemoryError的症狀。如果你的應用程序確實存在內存泄漏或者本來就加載了一些不合理的類,那麼所有這些配置都只是推遲問題出現的時間而已,實際也不會改善任何東西。

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