說(shuō)明:?Permgen(永久代) 屬于 JDK1.7 及之前版本的概念; 為了適應(yīng)Java程序的發(fā)展, JDK8以后的版本采用限制更少的 MetaSpace 來(lái)代替, 詳情請(qǐng)參考下一篇文章:?OutOfMemoryError系列(4): Metaspace逃沿。
這是本系列的第三篇文章, 相關(guān)文章列表:
OutOfMemoryError系列(1): Java heap space
OutOfMemoryError系列(2): GC overhead limit exceeded
OutOfMemoryError系列(3): Permgen space
OutOfMemoryError系列(4): Metaspace
JVM限制了Java程序的最大內(nèi)存使用量, 可以通過(guò)啟動(dòng)參數(shù)來(lái)配置萍丐。而Java的堆內(nèi)存被劃分為多個(gè)區(qū)域, 如下圖所示:
這些區(qū)域的最大值, 由JVM啟動(dòng)參數(shù)?-Xmx?和?-XX:MaxPermSize?指定. 如果沒(méi)有明確指定, 則根據(jù)操作系統(tǒng)平臺(tái)和物理內(nèi)存的大小來(lái)確定。
java.lang.OutOfMemoryError: PermGen space?錯(cuò)誤信息所表達(dá)的意思是:?永久代(Permanent Generation) 內(nèi)存區(qū)域已滿
我們先看看?PermGen?是用來(lái)干什么的计技。
在JDK1.7及之前的版本, 永久代(permanent generation) 主要用于存儲(chǔ)加載/緩存到內(nèi)存中的 class 定義, 包括 class 的 名稱(chēng)(name), 字段(fields), 方法(methods)和字節(jié)碼(method bytecode); 以及常量池(constant pool information); 對(duì)象數(shù)組(object arrays)/類(lèi)型數(shù)組(type arrays)所關(guān)聯(lián)的 class, 還有 JIT 編譯器優(yōu)化后的class信息等晒衩。
很容易看出, PermGen 的使用量和JVM加載到內(nèi)存中的 class 數(shù)量/大小有關(guān)嗤瞎∏酵幔可以說(shuō)?java.lang.OutOfMemoryError: PermGen space?的主要原因, 是加載到內(nèi)存中的 class 數(shù)量太多或體積太大。
我們知道, PermGen 空間的使用量, 與JVM加載的 class 數(shù)量有很大關(guān)系猫胁。下面的代碼演示了這種情況:
importjavassist.ClassPool;publicclassMicroGenerator{publicstaticvoidmain(String[] args)throwsException {for(inti =0; i <100_000_000; i++) {? ? ? generate("eu.plumbr.demo.Generated"+ i);? ? }? }publicstaticClassgenerate(String name)throwsException {? ? ClassPool pool = ClassPool.getDefault();returnpool.makeClass(name).toClass();? }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
這段代碼在 for 循環(huán)中, 動(dòng)態(tài)生成了很多class箱亿□诵浚可以看到, 使用?javassist?工具類(lèi)生成 class 是非常簡(jiǎn)單的弃秆。
執(zhí)行這段代碼, 會(huì)生成很多新的 class 并將其加載到內(nèi)存中, 隨著生成的class越來(lái)越多,將會(huì)占滿Permgen空間, 然后拋出?java.lang.OutOfMemoryError: Permgen space?錯(cuò)誤, 當(dāng)然, 也有可能會(huì)拋出其他類(lèi)型的 OutOfMemoryError。
要快速看到效果, 可以加上適當(dāng)?shù)腏VM啟動(dòng)參數(shù), 如:?-Xmx200M -XX:MaxPermSize=16M?等等髓帽。
Redeploy 時(shí)產(chǎn)生的 OutOfMemoryError
說(shuō)明:?如果在開(kāi)發(fā)時(shí)Tomcat產(chǎn)生警告菠赚,可以忽略。 生產(chǎn)環(huán)境建議不要 redploy,直接關(guān)閉/或Kill相關(guān)的JVM郑藏,然后從頭開(kāi)始啟動(dòng)即可衡查。
下面的情形更常見(jiàn),在重新部署web應(yīng)用時(shí), 很可能會(huì)引起?java.lang.OutOfMemoryError: Permgen space?錯(cuò)誤. 按道理說(shuō), redeploy 時(shí), Tomcat之類(lèi)的容器會(huì)使用新的 classloader 來(lái)加載新的 class, 讓垃圾收集器?將之前的 classloader (連同加載的class一起)清理掉,。
但實(shí)際情況可能并不樂(lè)觀, 很多第三方庫(kù), 以及某些受限的共享資源, 如 thread, JDBC驅(qū)動(dòng), 以及文件系統(tǒng)句柄(handles), 都會(huì)導(dǎo)致不能徹底卸載之前的 classloader. 那么在 redeploy 時(shí), 之前的class仍然駐留在PermGen中,?每次重新部署都會(huì)產(chǎn)生幾十MB必盖,甚至上百M(fèi)B的垃圾拌牲。
假設(shè)某個(gè)應(yīng)用在啟動(dòng)時(shí), 通過(guò)初始化代碼加載JDBC驅(qū)動(dòng)連接數(shù)據(jù)庫(kù). 根據(jù)JDBC規(guī)范, 驅(qū)動(dòng)會(huì)將自身注冊(cè)到?java.sql.DriverManager, 也就是將自身的一個(gè)實(shí)例(instance) 添加到?DriverManager?中的一個(gè) static 域。
那么, 當(dāng)應(yīng)用從容器中卸載時(shí),?java.sql.DriverManager?依然持有 JDBC實(shí)例(Tomcat經(jīng)常會(huì)發(fā)出警告), 而JDBC驅(qū)動(dòng)實(shí)例又持有?java.lang.Classloader?實(shí)例, 那么?垃圾收集器?也就沒(méi)辦法回收對(duì)應(yīng)的內(nèi)存空間歌粥。
而?java.lang.ClassLoader?實(shí)例持有著其加載的所有 class, 通常是幾十/上百 MB的內(nèi)存塌忽。可以看到, redeploy時(shí)會(huì)占用另一塊差不多大小的 PermGen 空間, 多次 redeploy 之后, 就會(huì)造成?java.lang.OutOfMemoryError: PermGen space?錯(cuò)誤, 在日志文件中, 你應(yīng)該會(huì)看到相關(guān)的錯(cuò)誤信息失驶。
1.?解決程序啟動(dòng)時(shí)產(chǎn)生的 OutOfMemoryError
在程序啟動(dòng)時(shí), 如果 PermGen 耗盡而產(chǎn)生 OutOfMemoryError 錯(cuò)誤, 那很容易解決. 增加 PermGen 的大小, 讓程序擁有更多的內(nèi)存來(lái)加載 class 即可. 修改?-XX:MaxPermSize?啟動(dòng)參數(shù), 類(lèi)似下面這樣:
java -XX:MaxPermSize=512mcom.yourcompany.YourClass
1
2
以上配置允許JVM使用的最大 PermGen 空間為?512MB, 如果還不夠, 就會(huì)拋出?OutOfMemoryError土居。
2.?解決 redeploy 時(shí)產(chǎn)生的 OutOfMemoryError
我們可以進(jìn)行堆轉(zhuǎn)儲(chǔ)分析(heap dump analysis) —— 在 redeploy 之后, 執(zhí)行堆轉(zhuǎn)儲(chǔ), 類(lèi)似下面這樣:
jmap -dump:format=b,file=dump.hprof
1
2
然后通過(guò)堆轉(zhuǎn)儲(chǔ)分析器(如強(qiáng)悍的 Eclipse MAT)加載 dump 得到的文件。找出重復(fù)的類(lèi), 特別是類(lèi)加載器(classloader)對(duì)應(yīng)的 class. 你可能需要比對(duì)所有的 classloader, 來(lái)找出當(dāng)前正在使用的那個(gè)嬉探。
Eclipse MAT 在各個(gè)平臺(tái)都有獨(dú)立安裝包. 大約50MB左右, 官網(wǎng)下載地址:?http://www.eclipse.org/mat/downloads.php
對(duì)于不使用的類(lèi)加載器(inactive classloader), 需要先確定最短路徑的?GC root?, 看看是哪一個(gè)阻止其被?垃圾收集器?所回收. 這樣才能找到問(wèn)題的根源. 如果是第三方庫(kù)的原因, 那么可以搜索 Google/StackOverflow 來(lái)查找解決方案. 如果是自己的代碼問(wèn)題, 則需要在恰當(dāng)?shù)臅r(shí)機(jī)來(lái)解除相關(guān)引用擦耀。
3.?解決運(yùn)行時(shí)產(chǎn)生的 OutOfMemoryError
如果在運(yùn)行的過(guò)程中發(fā)生 OutOfMemoryError, 首先需要確認(rèn)?GC是否能從PermGen中卸載class。 官方的JVM在這方面是相當(dāng)?shù)谋J?在加載class之后,就一直讓其駐留在內(nèi)存中,即使這個(gè)類(lèi)不再被使用). 但是, 現(xiàn)代的應(yīng)用程序在運(yùn)行過(guò)程中, 會(huì)動(dòng)態(tài)創(chuàng)建大量的class, 而這些class的生命周期基本上都很短暫, 舊版本的JVM 不能很好地處理這些問(wèn)題涩堤。那么我們就需要允許JVM卸載class眷蜓。使用下面的啟動(dòng)參數(shù):
-XX:+CMSClassUnloadingEnabled
1
2
默認(rèn)情況下?CMSClassUnloadingEnabled?的值為false, 所以需要明確指定。 啟用以后,?GC 將會(huì)清理?PermGen, 卸載無(wú)用的 class. 當(dāng)然, 這個(gè)選項(xiàng)只有在設(shè)置?UseConcMarkSweepGC?時(shí)生效胎围。 如果使用了?ParallelGC, 或者?Serial GC?時(shí), 那么需要切換為CMS:
-XX:+UseConcMarkSweepGC
1
如果確定 class 可以被卸載, 假若還存在 OutOfMemoryError, 那就需要進(jìn)行堆轉(zhuǎn)儲(chǔ)分析了, 類(lèi)似下面這種命令:
jmap -dump:file=dump.hprof,format=b
1
2
然后通過(guò)堆轉(zhuǎn)儲(chǔ)分析器(如 Eclipse MAT) 加載 heap dump账磺。找出最重的 classloader, 也就是加載 class 數(shù)量最多的那個(gè). 通過(guò)加載的 class 及對(duì)應(yīng)的實(shí)例數(shù)量, 比對(duì)類(lèi)加載器, 找出最靠前的部分, 挨個(gè)進(jìn)行分析。
對(duì)于每個(gè)有嫌疑的類(lèi), 都需要手動(dòng)跟蹤到生成這些類(lèi)的代碼中, 以定位問(wèn)題痊远。