3.1 Permgen space 概述
Java應用只允許使用有限的內存. 你的應用的內存大小是在啓動的時候指定好的. 進一步來說, Java內存被分成2個不同的區域, 如下圖:
這些區域, 包括perm區, 會在JVM啓動時設置. 如果你沒有設置, 會使用與平臺有關的默認配置.
java.lang.OutOfMemoryError: PermGen Space
消息表示永久代(Permgen)內存耗盡.
3.2 原因
要理解java.lang.OutOfMemoryError: PermGen Space
的原因, 我們需要理解這個特殊的內存區域是用來幹嘛.
實際上, 永久代主要是加載和存儲的類聲明. 包括組成類的類的名字和字段(fields), 方法的字節碼, 常量池信息, 對象數組和類型數組, 以及實時編譯(Just In Time compiler)優化.
從上邊的定義, 你可以推斷出PerGen大小需求取決於加載的類的數量和這些類聲明的大小. 因此我們可以說, java.lang.OutOfMemoryError: PermGen Space
的主要原因是: 太多類或者太大的類被加載到永久代.
3.3 示例
3.3.1 極簡案例
正如之前的描述, 永久代的使用量和加載的JVM裏的類的數量強相關. 下列代碼就是最直接的例子:
import javassist.ClassPool;
public class MicroGenerator {
public static void main(String[] args) throws Exception {
for (int i = 0;i<100_000_000;i++) {
genetate("eu.plumbr.demo.Generated" + i);
}
}
public static Class generate(String name) throws Exception {
ClassPool pool = ClassPool.getDefault();
return pool.makeClass(name).toClass();
}
}
在本例中,源代碼在運行時循環迭代並生成類. 類javassist
庫對生成的複雜性進行了處理.
上面的代碼將持續生成新的類並將其定義加載到永久代中直到該區被佔滿, 並且拋出java.lang.OutOfMemoryError: PermGen Space
。
3.3.2 重部署案例
一個更復雜和現實的例子, 我們經常會在應用重部署時出現java.lang.OutOfMemoryError: PermGen Space
錯誤. 當你重新部署一個應用時, 您的意圖是刪除以前的類加載器引用的所有以前加載的類,並將其替換新的類加載器加載新版本的類。
不幸的是, 很多第三方庫和處理不當的資源如線程, JDBC驅動或文件系統句柄使卸載以前使用的類加載器變得不可能. 這反過來意味着: 在每次重新部署期間,您的類的前一個版本仍然駐留在PermGen中,在每次重新部署時生成數十MB(甚至更多)的垃圾。
我們假設一個示例應用通過JDBC驅動連接到一個關係型數據庫. 當應用啓動時, 初始化代碼加載JDBC驅動來連接數據庫. 對應於規範,JDBC驅動程序將自己註冊到java.sql.DriverManager
。這個註冊包含存儲在一個靜態的驅動程序管理(DriverManager)字段中的一個驅動實例.
現在, 當應用從應用服務器卸載, java.sql.DriverManager
仍然會持有那個引用. 最後,我們對驅動類進行了實時引用,而驅動類又引用了用於加載應用程序的java.lang.Classloader
。
java.lang.Classloader
的那個實例仍然引用這個應用的所有的類, 通常會在Perm區裏佔用數十MB內存. 這也意味着: 只需幾次重新部署就可以填充一個常見大小的PermGen並在日誌中出現java.lang.OutOfMemoryError: PermGen Space
錯誤.
3.4 解決方案
3.4.1 解決初始化時的OutOfMemoryError
當由於PermGen耗盡導致在應用運行時出現OutOfMemoryError錯誤, 解決方案很簡單. 應用只需要更多的空間來加載所有類到Perm區, 因此我們只需要增加它的大小. 要這麼做, 調整應用啓動配置, 並添加(如果有就增加)-XX:MaxPermSize
參數如下:
java -XX:MaxPermSize=512m com.yourcompany.YourClass
上述配置會告訴JVM, Perm區在開始報OutOfMemoryError之前允許增大到512MB.
3.4.2 解決重部署時的OutOfMemoryError
當OutOfMemoryError就發生在你重新部署應用之後的時候, 你的應用有類加載器泄漏的問題. 這時, 你應該做heap dump分析 - 在重部署後做這個heap dump:
jmap -dump:format=b,file=dump.hprof <process-id>
然後用你最喜歡的heap dump 分析工具(Eclipse MAT是個好工具)打開. 在分析工具中, 你可以看重複的類(duplicate classes), 特別是你自己的應用的. 從那裏, 你需要處理所有的類加載器來找到當前活動的類加載器.
具體如何在MAT中看重複的類(duplicate classed)可以參考我的另一篇文章.
對於非活動的類加載器, 您需要通過從非活動類加載器獲取最短GC root路徑來確定阻止它們來進行gc的引用。 有了這些信息, 你就能定位root cause. 如果 root cause 是第三方庫, 你可以通過 Google/StackOverflow 來搜索是否這是一個已知問題來獲取patch或解決方法. 如果是你自己的代碼, 你需要避免違規引用.
3.4.3 解決運行時的 OutOfMemoryError
當應用在運行時PermGen內存溢出, 聯繫我就是最好的方式(@ ̄ー ̄@).
另一種可選的, 不用聯繫我的方法也是可行的. 在這種情況下第一步就是要檢查是否GC允許卸載來自PerGen的這些類. 一般JVM在這方面是相當保守的 – 類是永生不滅的. 所以一旦加載, 即使沒有代碼再使用它們, 它們仍然會呆在內存中. 這就會變成一個問題: 當應用創建了大量的動態類, 而且生成的這些類是不需要長久存在的. 在這種情況下, 允許JVM卸載類定義會有所幫助. 在你的啓動腳本種加入以下字段即可實現:
-XX:+CMSClassUnloadingEnabled
默認這是設爲false
的, 所以要啓用這個, 你需要顯示地在Java選項中設置下列參數. 如果你啓用了CMSClassUnloadingEnabled
, GC將也會清理PermGen, 移除不再需要使用的類. 要記住這個參數只在UseConcMarkSweepGC
也啓用的時候纔會生效. 所以, 當使用並行 GC, 或者, 我的天吶 – 串行GC時, 確保你指定你的GC策略到CMS通過:
-XX:+UseConcMarkSweepGC
如果類可以卸載, 問題仍然存在, 你應該做heap dump分析 – 用類似如下的命令:
jmap -dump:file=dump.hprof,format=b <process-id>
然後用你的最愛的heap dump分析工具(如: Eclipse MAT)打開這個dump, 找到加載最多類的類加載器. 從這個類加載器中, 您可以繼續提取加載的類,並通過實例對這些類進行排序,以獲得最大的懷疑項列表。
對於每個可疑項,您需要手工地追溯root cause到生成此類類的應用程序代碼.
後續會有一篇我通過Dynatrace分析某財險公司運行時的 Perm區OutOfMemoryError的案例.