內存溢出之PermGen OOM深刻分析

如今,網上關於討論PermGen OOM的資料不少,可是深刻分析PermGen區域內存溢出緣由的資料不多。本篇文章嘗試全面分析一下PermGen OOM的緣由,其中涉及到了Java虛擬機運行時數據區、類型裝載、類型卸載等,測試代碼涉及到了JMX協議。相關前提知識以下:
       一、Java類加載的基本原理
       二、Java類型卸載相關的知識,http://www.blogjava.net/zhuxing/archive/2008/07/24/217285.html
       三、簡要了解JMX協議,有關JMX協議能夠參加sun公司發佈的技術規範,對JMX協議作必定的瞭解對理解Java性能監控和調優功能的實現原理有很大幫助。

        
       【虛擬機運行時數據區介紹】
    
本部分將對Java虛擬機運行時數據區作一個簡單的介紹,着重說明PermGen區域(永久存儲區)存放的內容,並對運行時數據區的訪問方式作一個概括說明,爲後面深刻分析類型卸載和PermGen OOM作鋪墊。爲了更具備通用性,本部分將更多關注虛擬機協議自己,可能和具體的虛擬機實現有少量的出入。

        【運行時數據區分類】

        Java虛擬機的運行時數據區通常分類以下(不必定是物理劃分):   html

  •     堆:主要存放對象實例,線程共享
  •     棧:主要存儲特定線程的方法調用狀態,線程獨佔
  •     本地方法棧:存儲本地方法的調用狀態,線程獨佔
  •     PC寄存器:學過操做系統課程的都知道,線程獨佔
  •     方法區:主要存儲了類型信息,線程共享 

            方法區能夠簡單的等價爲所謂的PermGen區域(永久存儲區),在不少虛擬機相關的文檔中,也將其稱之爲"永久堆"(permanent heap),做爲堆空間的一部分存在。介於此,咱們能夠簡單說明一下咱們經常使用的幾個堆內存配置的參數關係:
        *-XX: PermSize:*永久堆(Pergen區域)大小默認值
        *-XX:MaxPermSize:*永久堆(Pergen區域)最大值
        *-Xms:*堆內存大小默認值
        *-Xmx:*堆內存最大值

            【運行時數據區訪問方式總結】
    java

        從開發者角度,虛擬機運行時數據區的訪問方式簡要概括以下:bootstrap

  •     活動的線程能夠經過對應的棧來訪問運行時數據區信息
  •     棧是堆訪問的入口
  •     堆上Java.lang.Class實例是訪問PermGen區域中類型信息的入口 
             
    1. 一個類型裝載以後會建立一個對應的java.lang.Class實例,這個實例自己和普通對象實例同樣存儲於堆中,我以爲之因此說是這是一種特殊的實例,某種程度上是由於其充當了訪問PermGen區域中類型信息的代理者。
    2. 圖中"Class類型實例"和"類加載器實例"分別是A類型對應的java.lang.Class實例和加載A類型的類加載器實例。
    3. 只要是有active的對象實例句柄,就可以訪問到對應的Class類型實例和類加載器實例,分別經過Object.getClass()方法和Class.getClassLoader()方法。
    4. 只要是有active的Class類型實例句柄,就可以訪問到對應的類加載器實例。

            【PermGen內存溢出深刻分析】

            【前提知識】    緩存

  •     由不一樣的類加載器實例加載的類型能夠等價爲徹底不一樣的類型,哪怕時同一類型類加載器的不一樣實例加載的,都會在PermGen區域分配相應的空間來存儲類型信息
  •     新類型加載時,會在PermGen區域申請相應的空間來存儲類型信息,類型被卸載後,PermGen區域上的垃圾收集會釋放對應的內存空間。PermGen區域和普通的堆空間同樣,也遵循垃圾收集的規律,因此,網上不少資料種關於PermGen區域空間的大小是隻增不減的說法是不正確的,後面會用相應的測試代碼來驗證和分析。
  •     一種類型被卸載的前提條件是:加載此類型的類加載器實例變爲不可達(unreachable)狀態,虛擬機協議中對應描述以下:
        A class or interface may be unloaded if and only if its class loader is unreachable. The bootstrap class loader is always reachable; as a result, system classes may never be unloaded.
    關於實例的*unreachable*狀態,大體能夠理解爲不能經過特定活動線程對應的棧出發經過引用計算來到達對應的實例,虛擬機協議中對應描述以下:
        _A reachable object is any object that can be accessed in any potential continuing
    computation from any live thread._
        結合上面的[虛擬機運行時數據區的介紹|],能夠得出結論:類型對應的普通實例、類型對應的java.lang.Class實例、加載此類型的ClassLoader實例,三者中有任何一種或者多種是reachable狀態的,那麼此類型就不可能被卸載。
  •     JMX協議提供了相應的API接口,用來在運行時查詢當前虛擬機實例的內存使用和類型加載等信息。這也是不少Java性能監控和分析工具的基礎,後面的測試程序中也有相應的代碼使用了JMX協議。

            
                【測試程序分析】       工具

  • 虛擬機器參數設置以下:
        -XX: PermSize=4M -XX:MaxPermSize=4M -verbose -verbose:gc
        設置-verbose參數是爲了獲取類型加載和卸載的信息
        設置-verbose:gc是爲了獲取垃圾收集的相關信息
  • 在D:/classes目錄下有一個簡單的類型ZhuXing對應的class字節碼,測試代碼中用URLClassLoader來加載此類型

     

            【測試程序一:模擬PermGen OOM】性能

     1  try  {
     2      // 準備url
     3     URL url  =   new  File( " D:/classes " ).toURL();
     4     URL[] urls  =  {url};
     5 
     6      // 獲取有關類型加載的JMX接口
     7      ClassLoadingMXBean loadingBean  =  ManagementFactory.getClassLoadingMXBean();
     8 
     9      // 用於緩存類加載器
    10      List < ClassLoader >  classLoaders  =   new  ArrayList < ClassLoader > ();
    11 
    12      while  ( true ) {
    13        // 加載類型並緩存類加載器實例
    14         ClassLoader classLoader  =   new  URLClassLoader(urls);
    15        classLoaders.add(classLoader);
    16        classLoader.loadClass( " ZhuXing " );
    17 
    18        // 顯示數量信息(共加載過的類型數目,當前還有效的類型數目,已經被卸載的類型數目)
    19        System.out.println( " total:  "   +  loadingBean.getTotalLoadedClassCount());
    20  System.out.println( " active:  "   +  loadingBean.getLoadedClassCount());
    21       System.out.println( " unloaded:  "   +  loadingBean.getUnloadedClassCount());
    22     }
    23  catch  (Exception e) {
    24     e.printStackTrace();
    25  }
    26 
    27 

         【測試程序一分析
    運行測試程序一,輸出信息以下(摘取了部分):
    ......
    [Loaded ZhuXing from [file:/D:/classes/]]
    total: 2914
    active: 2914
    unloaded: 0
    [Loaded ZhuXing from [file:/D:/classes/]]
    total: 2915
    active: 2915
    unloaded: 0
    [Full GC 4852K->4852K(8720K), 0.0993780 secs]
    [Full GC 4852K->4829K(8720K), 0.0999775 secs]
    [Full GC 4829K->4829K(8720K), 0.0989805 secs]
    [Full GC 4829K->4829K(8720K), 0.0997261 secs]
    ......
    Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    ......
    [Unloading class ZhuXing]
    ......
    [Loaded java.lang.Shutdown from D:\eos6\jdk1.5.0_09\jre\lib\rt.jar]
    [Loadedjava.lang.Shutdown$Lockfrom D:\eos6\jdk1.5.0_09\jre\lib\rt.jar

        
            針對以上摘錄的虛擬機器運行時信息,分析結論以下:測試

  •     一直在持續的加載類型ZhuXing,並且一直沒有卸載,直到PermGen OOM發生。類型ZhuXing沒法卸載的緣由,前面說明過,是因爲對應的類加載器實例一直是reachaable狀態,緩存對象實例或者java.lang.Class實例一樣能夠達到沒法卸載類型的效果。
  •     在PermGen OOM發生前,虛擬機進行了很是頻繁的垃圾收集,效果甚微
  •     在PermGen OOM發生後,卸載了類型ZhuXing,當前虛擬機實例退出


            【測試程序二:PermGen區域垃圾收集】ui

                和測試程序一相比,刪除了類加載器實例緩存的代碼
     1  try  {
     2        // 準備url
     3       URL url  =   new  File( " D:/classes " ).toURL();
     4       URL[] urls  =  {url};
     5 
     6        // 獲取有關類型加載的JMX接口
     7        ClassLoadingMXBean loadingBean  =  ManagementFactory.getClassLoadingMXBean();
     8 
     9        while  ( true ) {
    10        // 加載類型,不緩存類加載器實例
    11         new  URLClassLoader(urls).loadClass( " ZhuXing " );
    12        // 顯示數量信息(共加載過的類型數目,當前還有效的類型數目,已經被卸載的類型數目)
    13        System.out.println( " total:  "   +  loadingBean.getTotalLoadedClassCount());
    14       System.out.println( " active:  "   +  loadingBean.getLoadedClassCount());
    15       System.out.println( " unloaded:  "   +  loadingBean.getUnloadedClassCount());
    16      }
    17  catch  (Exception e) {
    18      e.printStackTrace();
    19  }
    20 
    21 

    測試程序二分析
    運行測試程序二很長時間,一直沒有發生PermGen OOM異常,輸出信息以下(摘取了部分):
    ...
    [Loaded ZhuXing from [file:/D:/classes/]]
    total: 19540
    active: 1052
    unloaded: 18488
    [Full GC 1563K->259K(2112K), 0.1758958 secs]
    ......
    [Unloading class ZhuXing]
    [Unloading class ZhuXing]
    [Unloading class ZhuXing]
    ......
    [GC 1968K->1563K(2112K), 0.0025266 secs]
    ......
    [Loaded ZhuXing from [file:/D:/classes/]]
    total: 21098
    active: 440
    unloaded: 20658
    ...
    針對以上摘錄的虛擬機器運行時信息,分析結論以下:url

    1. 類型ZhuXing在頻繁被加載的同時,也在頻繁被卸載,當被加載的類型達到了21098時,並無發生PermGen OOM,20658已經被卸載,堆內存的佔用比測試代碼一中小的多
    2. 中間進行的垃圾並非特別頻繁,可是垃圾收集的效果較爲明顯
    3. 類型被卸載以後,伴隨着PermGen區域上的垃圾收集和新類型的不斷被加載,PermGen區域中類型信息佔有的堆內存大小在有序的增大減少

            【PermGen OOM緣由總結】
                經過上面的[測試程序分析|],咱們發現PermGen OOM發生的緣由和類型裝載、類型卸載有直接的關係,能夠對PermGen OOM發生的緣由作以下大體的總結:
            一、PermGen區域分配的堆空間太小,能夠經過設置-XX: PermSize參數和-XX:MaxPermSize參數來解決。
  •         二、類型卸載不及時,過期無效的類型信息佔用了空間,咱們不妨稱其爲"永久堆"的內存泄漏,須要經過深刻分析類型卸載的原理來尋找對應的防範措施spa


            【常見的類加載器和類型卸載的可能性總結】
            經過前面的討論,咱們知道若是加載某種類型的類加載器實例沒有處於unreachable狀態,則該類型就不會被卸載,該類型不被卸載,則對應的類型信息在PermGen區域中佔有的堆內存就不會被釋放。下面,針對典型的Java應用分類,分析一下經常使用類加載器加載的類型被下載的可能性。

            【普通Java應用】
            啓動類加載器:因爲其負責加載虛擬機的核心類型,因此由其加載的類型在整個程序運行期間不可能被卸載,對應類型信息佔用的PermGen區域堆空間不可能獲得釋放。
            擴展類加載器:負責加載JDK擴展路徑下的類型,擴展類加載器同時又做爲系統類加載器的父類加載器,因此,由其加載的類型在整個程序運行期間基本上不可能被卸載,對應類型信息佔用的PermGen區域堆空間基本不可能獲得釋放。
            系統類加載器:負責加載程序類路徑上面的類型,由其加載的類型在整個程序運行期間基本上不可能被卸載,對應類型信息佔用的PermGen區域堆空間基本不可能獲得釋放。
            用戶自定義類加載器:對於其加載的類型,知足類型卸載要求的可能性比較容易控制,只要是其實例自己處於unreachable狀態,其加載的類型會被卸載,PermGen區域中對應的空間佔有也會被釋放。


            【插件開發】
            系統類加載器:因爲其負責加載虛擬機的核心類型,因此由其加載的類型在插件應用運行期間不可能被卸載,對應類型信息佔用的PermGen區域堆空間不可能獲得釋放。
            插件類加載器:系統插件類加載器負責加載OSGI實現的相關類型,因此由其加載的類型在插件應用運行期間不可能被卸載;用戶開發的插件所使用的默認插件類加載器,和特定的插件自己進行域綁定,插件之間存在必定的類型引用關係,而且特定插件在整個插件應用的運行時被中止的可能性也很小,因此類型卸載發生概率極小。
            用戶自定義類加載器:對於其加載的類型,知足類型卸載要求的可能性比較容易控制,只要是其實例自己處於unreachable狀態,其加載的類型會被卸載,PermGen區域中對應的空間佔有也會被釋放。

            【PermGen內存溢出的應對措施】
         
    經過上面的PermGen OOM的緣由的分析,不難看出對應的應對措施:

  •     合理的設置-XX: PermSize和-XX:MaxPermSize參數(主要的有效措施)
  •     有效的利用的虛擬機類型卸載的機制(針對程序進行調優)


            【合理設置參數(針對普通用戶和開發者)】
            經過設置合理的XX: PermSize和-XX:MaxPermSize參數值是減小和有效避免PermGen OOM發生的最有效最主要的措施,尤爲是針對普通用戶而言,這基本上是惟一的辦法。關於合理設置這兩個參數,建議以下:

  •      XX: PermSize參數的設置儘可能創建在基準測試的基礎之上,能夠利用監控工具對穩定運行期間PermGen區域的大小進行統計,取合理的平均值。網上的不少資料中,建議XX: PermSize和XX:MaxPermSize設置爲相同的數值,我的以爲這是不正確的,由於兩個參數的出發點是不同的。XX: PermSize設置的過大確定會在應用運行的大部分時間中浪費堆內存,有可能會明顯增長存放普通對象實例的堆空間的垃圾收集的次數。

  •     XX:MaxPermSize參數的設置應該着眼於PermGen區域使用的峯值,由於這是避免PermGen OOM的最後一道屏障,其設置最好也是創建在性能監控工具的統計結果之上。
  •     和虛擬機有關的性能參數較多的分爲兩類,一類是初始值或默認值,一類是峯值。若是該性能參數是會涉及到的虛擬機垃圾收集機制的,關於初始值或者默認值的設置儘可能要創建在測試基礎之上,儘可能作到在單次垃圾收集時間和垃圾收集頻率之間保持一個平衡,不然頗有可能拔苗助長。


             【有效利用虛擬機類型卸載機制(針對開發者)】
            此部分的建議能夠做爲開發者進行性能調優或者平常開發時候的參考,儘可能可以配合相應的性能監控工具進行:   

  •     檢查是否因爲程序設計自己上的缺陷,致使加載了大量實際上並不須要的類型。較新版本的Java虛擬機實現,通常都遵循動態解析的建議,因此不是人爲設計的缺陷,通常不會誘發加載了大量實際上並不須要的類型。結合插件開發的應用場景,我的以爲插件功能模塊的劃分(其中包括了插件依賴關係的設計和有關擴展點的擴展收集等)和第三方jar的使用多是誘發此問題的兩個重要根源。
  •     對象緩存的使用是否得當,經過前面的分析,咱們知道這多是致使類型不能被卸載的重要緣由。緩存的使用,既要認識到其能夠提升時間性能的有點,也要分析其可能會給普通對象堆空間和PermGen區域形成的負擔。
  •     自定義類加載器的合理使用,相關的幾個注意要點包括:
    1. 是否不恰當的利用的類型更新的特性,也就是說是否對類加載器實例的unreachable狀態作了有效的判斷。考慮以下場景,假設用戶開發了一個自定義類加載器來加載工程輸出目錄下的臨時類型,對臨時類型作了沒必要要的緩存,這確定會致使全部被加載過的臨時類型都不會獲得卸載,會直接加劇PermGen區域的負擔。
    2. 自定義類加載器和其餘已有類加載器的協做關係是否合理,是否合理的利用了Java類加載的雙親委派機制。咱們知道,不一樣的類加載器實例(哪怕是同一種類加載器類型的不一樣實例)加載的同一種自定義類型在虛擬機內部都會被放置到不一樣的命名空間中做爲不一樣類型來處理,因此合理的設置父類加載器變得很重要,不合理的設置會致使大量沒必要要的"新"類型被創造出來,何況這些沒必要要的"新"類型是否可以被及時卸載仍是個未知數。
  •     慎重檢查自定義類加載器實例是否被不恰當的緩存了,緣由不言而喻。  【後記】 寫這篇文章的初衷是爲了深刻的分析PermGen OOM發生的緣由,在深刻分析的基礎之上理解PermGen OOM的應對措施,從"爲何會發生PermGen OOM"到"到底爲何會發生PermGen OOM"。但願對你們更深刻的認識PermGen OOM和PermGen OOM的應對措施起到做用,謝謝!
  • 相關文章
    相關標籤/搜索