1. 程式人生 > >記憶體溢位之PermGen OOM深入分析和解決方案

記憶體溢位之PermGen OOM深入分析和解決方案

閱讀原文

*現在,網上關於討論PermGen OOM的資料很多,但是深入分析PermGen區域記憶體溢位原因的資料很少。本篇文章嘗試全面分析一下PermGen OOM的原因,其中涉及到了Java虛擬機器執行時資料區、型別裝載、型別解除安裝等,測試程式碼涉及到了JMX協議。
【知識準備】

  1. Java類載入相關的知識,去年的知識點《Java類載入原理簡析》中結合JDK的程式碼實現對Java類載入的原理做了比較深入的分析;
  2. Java型別解除安裝相關的知識,去年的知識點《Java虛擬機器型別解除安裝和型別更新解析 》中對結合Java虛擬機器規範對虛擬機器型別解除安裝和型別更新做了較為深入的分析;
  3. 有關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:*堆記憶體最大值

執行時資料區訪問方式總結

從開發者角度,虛擬機器執行時資料區的訪問方式簡要歸納如下:

  1. 活動的執行緒可以通過對應的棧來訪問執行時資料區資訊
  2. 棧是堆訪問的入口
  3. 堆上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記憶體溢位深入分析

在本部分,首先交代一下必要的前提知識,這也為理解後面的測試程式做鋪墊。

前提知識

  1. 由不同的類載入器例項載入的型別可以等價為完全不同的型別,哪怕時同一型別類載入器的不同例項載入的,都會在PermGen區域分配相應的空間來儲存型別資訊
  2. 新型別載入時,會在PermGen區域申請相應的空間來儲存型別資訊,型別被解除安裝後,PermGen區域上的垃圾收集會釋放對應的記憶體空間。PermGen區域和普通的堆空間一樣,也遵循垃圾收集的規律,所以,網上很多資料種關於PermGen區域空間的大小是隻增不減的說法是不正確的,後面會用相應的測試程式碼來驗證和分析。
  3. 一種型別被解除安裝的前提條件是:載入此型別的類載入器例項變為不可達(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狀態的,那麼此型別就不可能被解除安裝。
  4. JMX協議提供了相應的API介面,用來在執行時查詢當前虛擬機器例項的記憶體使用和型別載入等資訊。這也是很多Java效能監控和分析工具的基礎,後面的測試程式中也有相應的程式碼使用了JMX協議。

測試程式分析

【測試程式說明】

  1. 虛擬機器器引數設定如下:
    -XX: PermSize=4M -XX:MaxPermSize=4M -verbose -verbose:gc
    設定-verbose引數是為了獲取型別載入和解除安裝的資訊
    設定-verbose:gc是為了獲取垃圾收集的相關資訊
  2. 在D:/classes目錄下有一個簡單的型別ZhuXing對應的class位元組碼,測試程式碼中用URLClassLoader來載入此型別

【測試程式一:模擬PermGen OOM】
try
{
//準備url
URL url = new File("D:/classes").toURL();
URL[] urls = {url};

//獲取有關型別載入的JMX介面
ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();

//用於快取類載入器
List<ClassLoader> classLoaders = new ArrayList<ClassLoader>();

while (true) {
//載入型別並快取類載入器例項
ClassLoader classLoader = new URLClassLoader(urls);
classLoaders.add(classLoader);
classLoader.loadClass("ZhuXing");

//顯示數量資訊(共載入過的型別數目,當前還有效的型別數目,已經被解除安裝的型別數目)
System.out.println("total: " + loadingBean.getTotalLoadedClassCount());
System.out.println("active: " + loadingBean.getLoadedClassCount());
System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());
}
} catch (Exception e) {
e.printStackTrace();
}
測試程式一分析
執行測試程式一,輸出資訊如下(摘取了部分):
......
[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

針對以上摘錄的虛擬機器器執行時資訊,分析結論如下:

  1. 一直在持續的載入型別ZhuXing,而且一直沒有解除安裝,直到PermGen OOM發生。型別ZhuXing無法解除安裝的原因,前面說明過,是由於對應的類載入器例項一直是reachaable狀態,快取物件例項或者java.lang.Class例項同樣可以達到無法解除安裝型別的效果。
  2. 在PermGen OOM發生前,虛擬機器進行了非常頻繁的垃圾收集,效果甚微
  3. 在PermGen OOM發生後,解除安裝了型別ZhuXing,當前虛擬機器例項退出

【測試程式二:PermGen區域垃圾收集】
和測試程式一相比,刪除了類載入器例項快取的程式碼。
try {
//準備url
URL url = new File("D:/classes").toURL();
URL[] urls = {url};

//獲取有關型別載入的JMX介面
ClassLoadingMXBean loadingBean = ManagementFactory.getClassLoadingMXBean();

while (true) {
//載入型別,不快取類載入器例項
new URLClassLoader(urls).loadClass("ZhuXing");
//顯示數量資訊(共載入過的型別數目,當前還有效的型別數目,已經被解除安裝的型別數目)
System.out.println("total: " + loadingBean.getTotalLoadedClassCount());
System.out.println("active: " + loadingBean.getLoadedClassCount());
System.out.println("unloaded: " + loadingBean.getUnloadedClassCount());
}
} catch (Exception e) {
e.printStackTrace();
}

測試程式二分析
執行測試程式二很長時間,一直沒有發生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引數來解決。
  2. 型別解除安裝不及時,過時無效的型別資訊佔用了空間,我們不妨稱其為"永久堆"的記憶體洩漏,需要通過深入分析型別解除安裝的原理來尋找對應的防範措施。

常見的類載入器和型別解除安裝的可能性總結

通過前面的討論,我們知道如果載入某種型別的類載入器例項沒有處於unreachable狀態,則該型別就不會被解除安裝,該型別不被解除安裝,則對應的型別資訊在PermGen區域中佔有的堆記憶體就不會被釋放。下面,針對典型的Java應用分類,分析一下常用類載入器載入的型別被下載的可能性。
【普通Java應用】
系統類載入器:由於其負責載入虛擬機器的核心型別,所以由其載入的型別在整個程式執行期間不可能被解除安裝,對應型別資訊佔用的PermGen區域堆空間不可能得到釋放。
擴充套件類載入器:負責載入JDK擴充套件路徑下的型別,擴充套件類載入器同時又作為系統類載入器的父類載入器,所以,由其載入的型別在整個程式執行期間基本上不可能被解除安裝,對應型別資訊佔用的PermGen區域堆空間基本不可能得到釋放。
系統類載入器:負責載入程式類路徑上面的型別,由其載入的型別在整個程式執行期間基本上不可能被解除安裝,對應型別資訊佔用的PermGen區域堆空間基本不可能得到釋放。
使用者自定義類載入器:對於其載入的型別,滿足型別解除安裝要求的可能性比較容易控制,只要是其例項本身處於unreachable狀態,其載入的型別會被解除安裝,PermGen區域中對應的空間佔有也會被釋放。

【外掛開發】
系統類載入器:由於其負責載入虛擬機器的核心型別,所以由其載入的型別在外掛應用執行期間不可能被解除安裝,對應型別資訊佔用的PermGen區域堆空間不可能得到釋放。
外掛類載入器:系統外掛類載入器負責載入OSGI實現的相關型別,所以由其載入的型別在外掛應用執行期間不可能被解除安裝;使用者開發的外掛所使用的預設外掛類載入器,和特定的外掛本身進行域繫結,外掛之間存在一定的型別引用關係,並且特定外掛在整個外掛應用的執行時被停止的可能性也很小,所以型別解除安裝發生機率極小。
使用者自定義類載入器:對於其載入的型別,滿足型別解除安裝要求的可能性比較容易控制,只要是其例項本身處於unreachable狀態,其載入的型別會被解除安裝,PermGen區域中對應的空間佔有也會被釋放。

PermGen記憶體溢位的應對措施

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

  1. 合理的設定-XX: PermSize和-XX:MaxPermSize引數(主要的有效措施)
  2. 有效的利用的虛擬機器型別解除安裝的機制(針對程式進行調優)

合理設定引數(針對普通使用者和開發者)

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

  1. XX: PermSize引數的設定儘量建立在基準測試的基礎之上,可以利用監控工具對穩定執行期間PermGen區域的大小進行統計,取合理的平均值。網上的很多資料中,建議XX: PermSize和XX:MaxPermSize設定為相同的數值,個人覺得這是不正確的,因為兩個引數的出發點是不一樣的。XX: PermSize設定的過大肯定會在應用執行的大部分時間中浪費堆記憶體,有可能會明顯增加存放普通物件例項的堆空間的垃圾收集的次數。
  2. XX:MaxPermSize引數的設定應該著眼於PermGen區域使用的峰值,因為這是避免PermGen OOM的最後一道屏障,其設定最好也是建立在效能監控工具的統計結果之上。
  3. 和虛擬機器有關的效能引數較多的分為兩類,一類是初始值或預設值,一類是峰值。如果該效能引數是會涉及到的虛擬機器垃圾收集機制的,關於初始值或者預設值的設定儘量要建立在測試基礎之上,儘量做到在單次垃圾收集時間和垃圾收集頻率之間保持一個平衡,否則很有可能適得其反。

有效利用虛擬機器型別解除安裝機制(針對開發者)

此部分的建議可以作為開發者進行效能調優或者日常開發時候的參考,儘量能夠配合相應的效能監控工具進行:

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

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