【原創】內存溢出之PermGen OOM深入分析
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虛擬機的運行時數據區一般分類如下(不一定是物理劃分):
方法區可以簡單的等價為所謂的PermGen區域(永久存儲區),在很多虛擬機相關的文檔中,也將其稱之為"永久堆"(permanent heap),作為堆空間的一部分存在。介于此,我們可以簡單說明一下我們常用的幾個堆內存配置的參數關系:
*-XX: PermSize:*永久堆(Pergen區域)大小默認值
*-XX:MaxPermSize:*永久堆(Pergen區域)最大值
*-Xms:*堆內存大小默認值
*-Xmx:*堆內存最大值
【運行時數據區訪問方式總結】
從開發者角度,虛擬機運行時數據區的訪問方式簡要歸納如下:
- 一個類型裝載之后會創建一個對應的java.lang.Class實例,這個實例本身和普通對象實例一樣存儲于堆中,我覺得之所以說是這是一種特殊的實例,某種程度上是因為其充當了訪問PermGen區域中類型信息的代理者。
- 圖中"Class類型實例"和"類加載器實例"分別是A類型對應的java.lang.Class實例和加載A類型的類加載器實例。
- 只要是有active的對象實例句柄,就能夠訪問到對應的Class類型實例和類加載器實例,分別通過Object.getClass()方法和Class.getClassLoader()方法。
- 只要是有active的Class類型實例句柄,就能夠訪問到對應的類加載器實例。
【PermGen內存溢出深入分析】
【前提知識】
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狀態的,那么此類型就不可能被卸載。
【測試程序分析】
-XX: PermSize=4M -XX:MaxPermSize=4M -verbose -verbose:gc
設置-verbose參數是為了獲取類型加載和卸載的信息
設置-verbose:gc是為了獲取垃圾收集的相關信息
【測試程序一:模擬PermGen OOM】
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
針對以上摘錄的虛擬機器運行時信息,分析結論如下:
【測試程序二:PermGen區域垃圾收集】
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
...
針對以上摘錄的虛擬機器運行時信息,分析結論如下:
- 類型ZhuXing在頻繁被加載的同時,也在頻繁被卸載,當被加載的類型達到了21098時,并沒有發生PermGen OOM,20658已經被卸載,堆內存的占用比測試代碼一中小的多
- 中間進行的垃圾并不是特別頻繁,但是垃圾收集的效果較為明顯
- 類型被卸載之后,伴隨著PermGen區域上的垃圾收集和新類型的不斷被加載,PermGen區域中類型信息占有的堆內存大小在有序的增大減小
【PermGen OOM原因總結】
通過上面的[測試程序分析|],我們發現PermGen OOM發生的原因和類型裝載、類型卸載有直接的關系,可以對PermGen OOM發生的原因做如下大致的總結:
1、PermGen區域分配的堆空間過小,可以通過設置-XX: PermSize參數和-XX:MaxPermSize參數來解決。
2、類型卸載不及時,過時無效的類型信息占用了空間,我們不妨稱其為"永久堆"的內存泄漏,需要通過深入分析類型卸載的原理來尋找對應的防范措施
【常見的類加載器和類型卸載的可能性總結】
通過前面的討論,我們知道如果加載某種類型的類加載器實例沒有處于unreachable狀態,則該類型就不會被卸載,該類型不被卸載,則對應的類型信息在PermGen區域中占有的堆內存就不會被釋放。下面,針對典型的Java應用分類,分析一下常用類加載器加載的類型被下載的可能性。
【普通Java應用】
啟動類加載器:由于其負責加載虛擬機的核心類型,所以由其加載的類型在整個程序運行期間不可能被卸載,對應類型信息占用的PermGen區域堆空間不可能得到釋放。
擴展類加載器:負責加載JDK擴展路徑下的類型,擴展類加載器同時又作為系統類加載器的父類加載器,所以,由其加載的類型在整個程序運行期間基本上不可能被卸載,對應類型信息占用的PermGen區域堆空間基本不可能得到釋放。
系統類加載器:負責加載程序類路徑上面的類型,由其加載的類型在整個程序運行期間基本上不可能被卸載,對應類型信息占用的PermGen區域堆空間基本不可能得到釋放。
用戶自定義類加載器:對于其加載的類型,滿足類型卸載要求的可能性比較容易控制,只要是其實例本身處于unreachable狀態,其加載的類型會被卸載,PermGen區域中對應的空間占有也會被釋放。
【插件開發】
系統類加載器:由于其負責加載虛擬機的核心類型,所以由其加載的類型在插件應用運行期間不可能被卸載,對應類型信息占用的PermGen區域堆空間不可能得到釋放。
插件類加載器:系統插件類加載器負責加載OSGI實現的相關類型,所以由其加載的類型在插件應用運行期間不可能被卸載;用戶開發的插件所使用的默認插件類加載器,和特定的插件本身進行域綁定,插件之間存在一定的類型引用關系,并且特定插件在整個插件應用的運行時被停止的可能性也很小,所以類型卸載發生幾率極小。
用戶自定義類加載器:對于其加載的類型,滿足類型卸載要求的可能性比較容易控制,只要是其實例本身處于unreachable狀態,其加載的類型會被卸載,PermGen區域中對應的空間占有也會被釋放。
【PermGen內存溢出的應對措施】
通過上面的PermGen OOM的原因的分析,不難看出對應的應對措施:
【合理設置參數(針對普通用戶和開發者)】
通過設置合理的XX: PermSize和-XX:MaxPermSize參數值是減少和有效避免PermGen OOM發生的最有效最主要的措施,尤其是針對普通用戶而言,這基本上是唯一的辦法。關于合理設置這兩個參數,建議如下:
XX: PermSize參數的設置盡量建立在基準測試的基礎之上,可以利用監控工具對穩定運行期間PermGen區域的大小進行統計,取合理的平均值。網上的很多資料中,建議XX: PermSize和XX:MaxPermSize設置為相同的數值,個人覺得這是不正確的,因為兩個參數的出發點是不一樣的。XX: PermSize設置的過大肯定會在應用運行的大部分時間中浪費堆內存,有可能會明顯增加存放普通對象實例的堆空間的垃圾收集的次數。
【有效利用虛擬機類型卸載機制(針對開發者)】
此部分的建議可以作為開發者進行性能調優或者日常開發時候的參考,盡量能夠配合相應的性能監控工具進行:
- 是否不恰當的利用的類型更新的特性,也就是說是否對類加載器實例的unreachable狀態做了有效的判斷??紤]如下場景,假設用戶開發了一個自定義類加載器來加載工程輸出目錄下的臨時類型,對臨時類型做了不必要的緩存,這肯定會導致所有被加載過的臨時類型都不會得到卸載,會直接加重PermGen區域的負擔。
- 自定義類加載器和其他已有類加載器的協作關系是否合理,是否合理的利用了Java類加載的雙親委派機制。我們知道,不同的類加載器實例(哪怕是同一種類加載器類型的不同實例)加載的同一種自定義類型在虛擬機內部都會被放置到不同的命名空間中作為不同類型來處理,所以合理的設置父類加載器變得很重要,不合理的設置會導致大量不必要的"新"類型被創造出來,況且這些不必要的"新"類型是否能夠被及時卸載還是個未知數。
【后記】
寫這篇文章的初衷是為了深入的分析PermGen OOM發生的原因,在深入分析的基礎之上理解PermGen OOM的應對措施,從"為什么會發生PermGen OOM"到"到底為什么會發生PermGen OOM"。希望對大家更深入的認識PermGen OOM和PermGen OOM的應對措施起到作用,謝謝!
本博客中的所有文章、隨筆除了標題中含有引用或者轉載字樣的,其他均為原創。轉載請注明出處,謝謝!
posted on 2008-08-01 19:31 zhuxing 閱讀(4858) 評論(10) 編輯 收藏 所屬分類: Java