Live a simple life

          沉默(zhu_xing@live.cn)
          隨筆 - 48, 文章 - 0, 評論 - 132, 引用 - 0
          數據加載中……

          【原創】內存溢出之PermGen OOM深入分析

                  現在,網上關于討論PermGen OOM的資料很多,但是深入分析PermGen區域內存溢出原因的資料很少。本篇文章嘗試全面分析一下PermGen OOM的原因,其中涉及到了Java虛擬機運行時數據區、類型裝載、類型卸載等,測試代碼涉及到了JMX協議。相關前提知識如下:
                 1、Java類加載的基本原理
                 2、Java類型卸載相關的知識,http://www.aygfsteel.com/zhuxing/archive/2008/07/24/217285.html
                 3、簡要了解JMX協議,有關JMX協議可以參加sun公司發布的技術規范,對JMX協議做一定的了解對理解Java性能監控和調優功能的實現原理有很大幫助。

                  
                 【虛擬機運行時數據區介紹】
              
          本部分將對Java虛擬機運行時數據區做一個簡單的介紹,著重說明PermGen區域(永久存儲區)存放的內容,并對運行時數據區的訪問方式做一個歸納說明,為后面深入分析類型卸載和PermGen OOM做鋪墊。為了更具有通用性,本部分將更多關注虛擬機協議本身,可能和具體的虛擬機實現有少許的出入。

                  【運行時數據區分類】

                  Java虛擬機的運行時數據區一般分類如下(不一定是物理劃分):   

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

                  方法區可以簡單的等價為所謂的PermGen區域(永久存儲區),在很多虛擬機相關的文檔中,也將其稱之為"永久堆"(permanent heap),作為堆空間的一部分存在。介于此,我們可以簡單說明一下我們常用的幾個堆內存配置的參數關系:
              *-XX: PermSize:*永久堆(Pergen區域)大小默認值
              *-XX:MaxPermSize:*永久堆(Pergen區域)最大值
              *-Xms:*堆內存大小默認值
              *-Xmx:*堆內存最大值

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

              從開發者角度,虛擬機運行時數據區的訪問方式簡要歸納如下:

        6.     活動的線程可以通過對應的棧來訪問運行時數據區信息
        7.     棧是堆訪問的入口
        8.     堆上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內存溢出深入分析】

                  【前提知識】    

        9.     由不同的類加載器實例加載的類型可以等價為完全不同的類型,哪怕時同一類型類加載器的不同實例加載的,都會在PermGen區域分配相應的空間來存儲類型信息
        10.     新類型加載時,會在PermGen區域申請相應的空間來存儲類型信息,類型被卸載后,PermGen區域上的垃圾收集會釋放對應的內存空間。PermGen區域和普通的堆空間一樣,也遵循垃圾收集的規律,所以,網上很多資料種關于PermGen區域空間的大小是只增不減的說法是不正確的,后面會用相應的測試代碼來驗證和分析。
        11.     一種類型被卸載的前提條件是:加載此類型的類加載器實例變為不可達(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狀態的,那么此類型就不可能被卸載。
        12.     JMX協議提供了相應的API接口,用來在運行時查詢當前虛擬機實例的內存使用和類型加載等信息。這也是很多Java性能監控和分析工具的基礎,后面的測試程序中也有相應的代碼使用了JMX協議。

                  
                      【測試程序分析】       

        13. 虛擬機器參數設置如下:
              -XX: PermSize=4M -XX:MaxPermSize=4M -verbose -verbose:gc
              設置-verbose參數是為了獲取類型加載和卸載的信息
              設置-verbose:gc是為了獲取垃圾收集的相關信息
        14. 在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

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

        15.     一直在持續的加載類型ZhuXing,而且一直沒有卸載,直到PermGen OOM發生。類型ZhuXing無法卸載的原因,前面說明過,是由于對應的類加載器實例一直是reachaable狀態,緩存對象實例或者java.lang.Class實例同樣可以達到無法卸載類型的效果。
        16.     在PermGen OOM發生前,虛擬機進行了非常頻繁的垃圾收集,效果甚微
        17.     在PermGen OOM發生后,卸載了類型ZhuXing,當前虛擬機實例退出


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

                      和測試程序一相比,刪除了類加載器實例緩存的代碼
           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
          ...
          針對以上摘錄的虛擬機器運行時信息,分析結論如下:

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

                  【PermGen OOM原因總結】
                      通過上面的[測試程序分析|],我們發現PermGen OOM發生的原因和類型裝載、類型卸載有直接的關系,可以對PermGen OOM發生的原因做如下大致的總結:
                  1、PermGen區域分配的堆空間過小,可以通過設置-XX: PermSize參數和-XX:MaxPermSize參數來解決。
        18.         2、類型卸載不及時,過時無效的類型信息占用了空間,我們不妨稱其為"永久堆"的內存泄漏,需要通過深入分析類型卸載的原理來尋找對應的防范措施


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

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


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

                  【PermGen內存溢出的應對措施】
               
          通過上面的PermGen OOM的原因的分析,不難看出對應的應對措施:

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


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

        21.      XX: PermSize參數的設置盡量建立在基準測試的基礎之上,可以利用監控工具對穩定運行期間PermGen區域的大小進行統計,取合理的平均值。網上的很多資料中,建議XX: PermSize和XX:MaxPermSize設置為相同的數值,個人覺得這是不正確的,因為兩個參數的出發點是不一樣的。XX: PermSize設置的過大肯定會在應用運行的大部分時間中浪費堆內存,有可能會明顯增加存放普通對象實例的堆空間的垃圾收集的次數。

        22.     XX:MaxPermSize參數的設置應該著眼于PermGen區域使用的峰值,因為這是避免PermGen OOM的最后一道屏障,其設置最好也是建立在性能監控工具的統計結果之上。
        23.     和虛擬機有關的性能參數較多的分為兩類,一類是初始值或默認值,一類是峰值。如果該性能參數是會涉及到的虛擬機垃圾收集機制的,關于初始值或者默認值的設置盡量要建立在測試基礎之上,盡量做到在單次垃圾收集時間和垃圾收集頻率之間保持一個平衡,否則很有可能適得其反。


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

        24.     檢查是否由于程序設計本身上的缺陷,導致加載了大量實際上并不需要的類型。較新版本的Java虛擬機實現,一般都遵循動態解析的建議,所以不是人為設計的缺陷,一般不會誘發加載了大量實際上并不需要的類型。結合插件開發的應用場景,個人覺得插件功能模塊的劃分(其中包括了插件依賴關系的設計和有關擴展點的擴展收集等)和第三方jar的使用可能是誘發此問題的兩個重要根源。
        25.     對象緩存的使用是否得當,通過前面的分析,我們知道這可能是導致類型不能被卸載的重要原因。緩存的使用,既要認識到其可以提高時間性能的有點,也要分析其可能會給普通對象堆空間和PermGen區域造成的負擔。
        26.     自定義類加載器的合理使用,相關的幾個注意要點包括:
          1. 是否不恰當的利用的類型更新的特性,也就是說是否對類加載器實例的unreachable狀態做了有效的判斷??紤]如下場景,假設用戶開發了一個自定義類加載器來加載工程輸出目錄下的臨時類型,對臨時類型做了不必要的緩存,這肯定會導致所有被加載過的臨時類型都不會得到卸載,會直接加重PermGen區域的負擔。
          2. 自定義類加載器和其他已有類加載器的協作關系是否合理,是否合理的利用了Java類加載的雙親委派機制。我們知道,不同的類加載器實例(哪怕是同一種類加載器類型的不同實例)加載的同一種自定義類型在虛擬機內部都會被放置到不同的命名空間中作為不同類型來處理,所以合理的設置父類加載器變得很重要,不合理的設置會導致大量不必要的"新"類型被創造出來,況且這些不必要的"新"類型是否能夠被及時卸載還是個未知數。
        27.     慎重檢查自定義類加載器實例是否被不恰當的緩存了,原因不言而喻。

          【后記】
          寫這篇文章的初衷是為了深入的分析PermGen OOM發生的原因,在深入分析的基礎之上理解PermGen OOM的應對措施,從"為什么會發生PermGen OOM"到"到底為什么會發生PermGen OOM"。希望對大家更深入的認識PermGen OOM和PermGen OOM的應對措施起到作用,謝謝!


        28. 本博客中的所有文章、隨筆除了標題中含有引用或者轉載字樣的,其他均為原創。轉載請注明出處,謝謝!

          posted on 2008-08-01 19:31 zhuxing 閱讀(4858) 評論(10)  編輯  收藏 所屬分類: Java

          評論

          # re: 【原創】內存溢出之PermGen OOM深入分析  回復  更多評論   

          有筆誤
          系統類加載器:由于其負責加載虛擬機的核心類型
          這里不應該是系統類加載器吧,應該是boostrap加載器。
          2008-08-01 20:40 | 鬼狗

          # re: 【原創】內存溢出之PermGen OOM深入分析  回復  更多評論   

          @鬼狗

          是筆誤,因該是啟動類加載器,已經修改
          2008-08-02 12:55 | zhuxing

          # re: 【原創】內存溢出之PermGen OOM深入分析  回復  更多評論   

          樓主的圖掛了,用的是局域網
          2008-08-04 10:43 | d

          # re: 【原創】內存溢出之PermGen OOM深入分析[未登錄]  回復  更多評論   

          樓主把圖片的問題解決掉就好了
          2008-08-04 16:17 | mark

          # re: 【原創】內存溢出之PermGen OOM深入分析  回復  更多評論   

          @mark
          圖片放上去了
          2008-08-08 10:23 | zhuxing

          # re: 【原創】內存溢出之PermGen OOM深入分析  回復  更多評論   

          還是看不到圖片的 做個測試啊
          2008-08-30 19:52 | advincenting

          # re: 【原創】內存溢出之PermGen OOM深入分析[未登錄]  回復  更多評論   

          老大..你的文章寫的非常好..
          看了您的有關 classLoader load & classLoader unload & PermGen OOM
          這幾篇..都非常不錯哈..
          獲益良多..非常感謝..繼續支持你..
          2008-10-14 14:09 | qq

          # re: 【原創】內存溢出之PermGen OOM深入分析  回復  更多評論   

          恩, 寫的確實不錯, 這3篇文章...
          2008-10-14 14:12 | moxiso

          # re: 【原創】內存溢出之PermGen OOM深入分析  回復  更多評論   

          寫得不錯!~
          2009-02-20 17:41 | Adam

          # re: 【原創】內存溢出之PermGen OOM深入分析  回復  更多評論   

          把簡單問題搞復雜了!·
          2009-06-18 10:44 | 式樣z
          主站蜘蛛池模板: 阿合奇县| 乐业县| 涡阳县| 临江市| 滁州市| 青铜峡市| 汨罗市| 时尚| 普宁市| 永川市| 康乐县| 晋州市| 松原市| 山西省| 安达市| 木兰县| 新民市| 昌图县| 阿拉善左旗| 秦皇岛市| 大连市| 吴旗县| 阳江市| 竹山县| 河北省| 洛扎县| 江阴市| 吴旗县| 汾西县| 宾川县| 江山市| 新竹县| 兰考县| 萨迦县| 河间市| 婺源县| 新郑市| 重庆市| 昌平区| 凌源市| 循化|