【原創(chuàng)】內(nèi)存溢出之PermGen OOM深入分析
1、Java類加載的基本原理
2、Java類型卸載相關(guān)的知識,http://www.aygfsteel.com/zhuxing/archive/2008/07/24/217285.html
3、簡要了解JMX協(xié)議,有關(guān)JMX協(xié)議可以參加sun公司發(fā)布的技術(shù)規(guī)范,對JMX協(xié)議做一定的了解對理解Java性能監(jiān)控和調(diào)優(yōu)功能的實現(xiàn)原理有很大幫助。
【虛擬機運行時數(shù)據(jù)區(qū)介紹】
本部分將對Java虛擬機運行時數(shù)據(jù)區(qū)做一個簡單的介紹,著重說明PermGen區(qū)域(永久存儲區(qū))存放的內(nèi)容,并對運行時數(shù)據(jù)區(qū)的訪問方式做一個歸納說明,為后面深入分析類型卸載和PermGen OOM做鋪墊。為了更具有通用性,本部分將更多關(guān)注虛擬機協(xié)議本身,可能和具體的虛擬機實現(xiàn)有少許的出入。
【運行時數(shù)據(jù)區(qū)分類】
Java虛擬機的運行時數(shù)據(jù)區(qū)一般分類如下(不一定是物理劃分):
方法區(qū)可以簡單的等價為所謂的PermGen區(qū)域(永久存儲區(qū)),在很多虛擬機相關(guān)的文檔中,也將其稱之為"永久堆"(permanent heap),作為堆空間的一部分存在。介于此,我們可以簡單說明一下我們常用的幾個堆內(nèi)存配置的參數(shù)關(guān)系:
*-XX: PermSize:*永久堆(Pergen區(qū)域)大小默認(rèn)值
*-XX:MaxPermSize:*永久堆(Pergen區(qū)域)最大值
*-Xms:*堆內(nèi)存大小默認(rèn)值
*-Xmx:*堆內(nèi)存最大值
【運行時數(shù)據(jù)區(qū)訪問方式總結(jié)】
從開發(fā)者角度,虛擬機運行時數(shù)據(jù)區(qū)的訪問方式簡要歸納如下:
- 一個類型裝載之后會創(chuàng)建一個對應(yīng)的java.lang.Class實例,這個實例本身和普通對象實例一樣存儲于堆中,我覺得之所以說是這是一種特殊的實例,某種程度上是因為其充當(dāng)了訪問PermGen區(qū)域中類型信息的代理者。
- 圖中"Class類型實例"和"類加載器實例"分別是A類型對應(yīng)的java.lang.Class實例和加載A類型的類加載器實例。
- 只要是有active的對象實例句柄,就能夠訪問到對應(yīng)的Class類型實例和類加載器實例,分別通過Object.getClass()方法和Class.getClassLoader()方法。
- 只要是有active的Class類型實例句柄,就能夠訪問到對應(yīng)的類加載器實例。
【PermGen內(nèi)存溢出深入分析】
【前提知識】
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.
關(guān)于實例的*unreachable*狀態(tài),大致可以理解為不能通過特定活動線程對應(yīng)的棧出發(fā)通過引用計算來到達(dá)對應(yīng)的實例,虛擬機協(xié)議中對應(yīng)描述如下:
_A reachable object is any object that can be accessed in any potential continuing
computation from any live thread._
結(jié)合上面的[虛擬機運行時數(shù)據(jù)區(qū)的介紹|],可以得出結(jié)論:類型對應(yīng)的普通實例、類型對應(yīng)的java.lang.Class實例、加載此類型的ClassLoader實例,三者中有任何一種或者多種是reachable狀態(tài)的,那么此類型就不可能被卸載。
【測試程序分析】
-XX: PermSize=4M -XX:MaxPermSize=4M -verbose -verbose:gc
設(shè)置-verbose參數(shù)是為了獲取類型加載和卸載的信息
設(shè)置-verbose:gc是為了獲取垃圾收集的相關(guān)信息
【測試程序一:模擬PermGen OOM】
2 //準(zhǔn)備url
3 URL url = new File("D:/classes").toURL();
4 URL[] urls = {url};
5
6 //獲取有關(guān)類型加載的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 //顯示數(shù)量信息(共加載過的類型數(shù)目,當(dāng)前還有效的類型數(shù)目,已經(jīng)被卸載的類型數(shù)目)
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
針對以上摘錄的虛擬機器運行時信息,分析結(jié)論如下:
【測試程序二:PermGen區(qū)域垃圾收集】
2 //準(zhǔn)備url
3 URL url = new File("D:/classes").toURL();
4 URL[] urls = {url};
5
6 //獲取有關(guān)類型加載的JMX接口
7 ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();
8
9 while (true) {
10 //加載類型,不緩存類加載器實例
11 new URLClassLoader(urls).loadClass("ZhuXing");
12 //顯示數(shù)量信息(共加載過的類型數(shù)目,當(dāng)前還有效的類型數(shù)目,已經(jīng)被卸載的類型數(shù)目)
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
【測試程序二分析】
運行測試程序二很長時間,一直沒有發(fā)生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
...
針對以上摘錄的虛擬機器運行時信息,分析結(jié)論如下:
- 類型ZhuXing在頻繁被加載的同時,也在頻繁被卸載,當(dāng)被加載的類型達(dá)到了21098時,并沒有發(fā)生PermGen OOM,20658已經(jīng)被卸載,堆內(nèi)存的占用比測試代碼一中小的多
- 中間進(jìn)行的垃圾并不是特別頻繁,但是垃圾收集的效果較為明顯
- 類型被卸載之后,伴隨著PermGen區(qū)域上的垃圾收集和新類型的不斷被加載,PermGen區(qū)域中類型信息占有的堆內(nèi)存大小在有序的增大減小
【PermGen OOM原因總結(jié)】
通過上面的[測試程序分析|],我們發(fā)現(xiàn)PermGen OOM發(fā)生的原因和類型裝載、類型卸載有直接的關(guān)系,可以對PermGen OOM發(fā)生的原因做如下大致的總結(jié):
1、PermGen區(qū)域分配的堆空間過小,可以通過設(shè)置-XX: PermSize參數(shù)和-XX:MaxPermSize參數(shù)來解決。
2、類型卸載不及時,過時無效的類型信息占用了空間,我們不妨稱其為"永久堆"的內(nèi)存泄漏,需要通過深入分析類型卸載的原理來尋找對應(yīng)的防范措施
【常見的類加載器和類型卸載的可能性總結(jié)】
通過前面的討論,我們知道如果加載某種類型的類加載器實例沒有處于unreachable狀態(tài),則該類型就不會被卸載,該類型不被卸載,則對應(yīng)的類型信息在PermGen區(qū)域中占有的堆內(nèi)存就不會被釋放。下面,針對典型的Java應(yīng)用分類,分析一下常用類加載器加載的類型被下載的可能性。
【普通Java應(yīng)用】
啟動類加載器:由于其負(fù)責(zé)加載虛擬機的核心類型,所以由其加載的類型在整個程序運行期間不可能被卸載,對應(yīng)類型信息占用的PermGen區(qū)域堆空間不可能得到釋放。
擴(kuò)展類加載器:負(fù)責(zé)加載JDK擴(kuò)展路徑下的類型,擴(kuò)展類加載器同時又作為系統(tǒng)類加載器的父類加載器,所以,由其加載的類型在整個程序運行期間基本上不可能被卸載,對應(yīng)類型信息占用的PermGen區(qū)域堆空間基本不可能得到釋放。
系統(tǒng)類加載器:負(fù)責(zé)加載程序類路徑上面的類型,由其加載的類型在整個程序運行期間基本上不可能被卸載,對應(yīng)類型信息占用的PermGen區(qū)域堆空間基本不可能得到釋放。
用戶自定義類加載器:對于其加載的類型,滿足類型卸載要求的可能性比較容易控制,只要是其實例本身處于unreachable狀態(tài),其加載的類型會被卸載,PermGen區(qū)域中對應(yīng)的空間占有也會被釋放。
【插件開發(fā)】
系統(tǒng)類加載器:由于其負(fù)責(zé)加載虛擬機的核心類型,所以由其加載的類型在插件應(yīng)用運行期間不可能被卸載,對應(yīng)類型信息占用的PermGen區(qū)域堆空間不可能得到釋放。
插件類加載器:系統(tǒng)插件類加載器負(fù)責(zé)加載OSGI實現(xiàn)的相關(guān)類型,所以由其加載的類型在插件應(yīng)用運行期間不可能被卸載;用戶開發(fā)的插件所使用的默認(rèn)插件類加載器,和特定的插件本身進(jìn)行域綁定,插件之間存在一定的類型引用關(guān)系,并且特定插件在整個插件應(yīng)用的運行時被停止的可能性也很小,所以類型卸載發(fā)生幾率極小。
用戶自定義類加載器:對于其加載的類型,滿足類型卸載要求的可能性比較容易控制,只要是其實例本身處于unreachable狀態(tài),其加載的類型會被卸載,PermGen區(qū)域中對應(yīng)的空間占有也會被釋放。
【PermGen內(nèi)存溢出的應(yīng)對措施】
通過上面的PermGen OOM的原因的分析,不難看出對應(yīng)的應(yīng)對措施:
【合理設(shè)置參數(shù)(針對普通用戶和開發(fā)者)】
通過設(shè)置合理的XX: PermSize和-XX:MaxPermSize參數(shù)值是減少和有效避免PermGen OOM發(fā)生的最有效最主要的措施,尤其是針對普通用戶而言,這基本上是唯一的辦法。關(guān)于合理設(shè)置這兩個參數(shù),建議如下:
XX: PermSize參數(shù)的設(shè)置盡量建立在基準(zhǔn)測試的基礎(chǔ)之上,可以利用監(jiān)控工具對穩(wěn)定運行期間PermGen區(qū)域的大小進(jìn)行統(tǒng)計,取合理的平均值。網(wǎng)上的很多資料中,建議XX: PermSize和XX:MaxPermSize設(shè)置為相同的數(shù)值,個人覺得這是不正確的,因為兩個參數(shù)的出發(fā)點是不一樣的。XX: PermSize設(shè)置的過大肯定會在應(yīng)用運行的大部分時間中浪費堆內(nèi)存,有可能會明顯增加存放普通對象實例的堆空間的垃圾收集的次數(shù)。
【有效利用虛擬機類型卸載機制(針對開發(fā)者)】
此部分的建議可以作為開發(fā)者進(jìn)行性能調(diào)優(yōu)或者日常開發(fā)時候的參考,盡量能夠配合相應(yīng)的性能監(jiān)控工具進(jìn)行:
- 是否不恰當(dāng)?shù)睦玫念愋透碌奶匦?,也就是說是否對類加載器實例的unreachable狀態(tài)做了有效的判斷??紤]如下場景,假設(shè)用戶開發(fā)了一個自定義類加載器來加載工程輸出目錄下的臨時類型,對臨時類型做了不必要的緩存,這肯定會導(dǎo)致所有被加載過的臨時類型都不會得到卸載,會直接加重PermGen區(qū)域的負(fù)擔(dān)。
- 自定義類加載器和其他已有類加載器的協(xié)作關(guān)系是否合理,是否合理的利用了Java類加載的雙親委派機制。我們知道,不同的類加載器實例(哪怕是同一種類加載器類型的不同實例)加載的同一種自定義類型在虛擬機內(nèi)部都會被放置到不同的命名空間中作為不同類型來處理,所以合理的設(shè)置父類加載器變得很重要,不合理的設(shè)置會導(dǎo)致大量不必要的"新"類型被創(chuàng)造出來,況且這些不必要的"新"類型是否能夠被及時卸載還是個未知數(shù)。
【后記】
寫這篇文章的初衷是為了深入的分析PermGen OOM發(fā)生的原因,在深入分析的基礎(chǔ)之上理解PermGen OOM的應(yīng)對措施,從"為什么會發(fā)生PermGen OOM"到"到底為什么會發(fā)生PermGen OOM"。希望對大家更深入的認(rèn)識PermGen OOM和PermGen OOM的應(yīng)對措施起到作用,謝謝!
本博客中的所有文章、隨筆除了標(biāo)題中含有引用或者轉(zhuǎn)載字樣的,其他均為原創(chuàng)。轉(zhuǎn)載請注明出處,謝謝!
posted on 2008-08-01 19:31 zhuxing 閱讀(4869) 評論(10) 編輯 收藏 所屬分類: Java