錢宇虹
摘要:Java程序運行時可能會遇到java.lang.OutOfMemoryError: PermGen space錯誤;JavaEE服務器(如tomcat、jboss等)在加載war包或ear包時也可能會出現(xiàn)這種錯誤,這些都是由于永久保存區(qū)域內(nèi)存不足導致的。本文分析了Java程序的運行機制和JVM的內(nèi)存結構,解釋了什么是permanent generation space,分析了PermGen space錯誤的常見原因,并給出避免這一錯誤的解決辦法。
關鍵詞:JVM; 類加載器; OutOfMemoryError:PermGen space
Java 程序的運行機制與普通程序,如C或C++ 程序的運行機制有很大的區(qū)別。
普通程序運行之前必須首先編譯成可執(zhí)行的二進制碼或機器碼。機器碼是與底層的硬件結構相關的,即使書寫源代碼的時候沒有利用平臺特定的擴展語言,如特定的文件訪問或圖形用戶界面,生成的機器碼仍然被綁定到一個特定的硬件平臺,從而只能運行在那個體系結構上,也就是說,為Sun工作站編譯的機器碼不能運行在PC機上,為PC機編譯的機器碼不能運行在蘋果機上,以此類推。
與此相反,Java源代碼不是為某種特定平臺編譯的,而是編譯成與平臺無關的字節(jié)碼(Byte Code),這種字節(jié)碼叫做Java Class(Java 類文件) 。在任何一種操作系統(tǒng)平臺上的字節(jié)碼都是一樣的,因此都可以在任何支持Java 虛擬機(JVM)的計算機上運行。JVM 直接受操作系統(tǒng)控制(而不是Java程序直接受操作系統(tǒng)控制),它負責將字節(jié)碼轉換成在特定平臺上能夠運行的機器碼。正是由于這個原因,Java做到了“write once, run anywhere”(書寫一次,到處運行)[1]。
運行Java程序時,首先需要由JVM把Java class加載到JVM里面。通常情況下,我們不會去關注JVM的內(nèi)部工作細節(jié)而只是直接拿來使用。然而,作為一個Java開發(fā)人員,你經(jīng)常會遇到與內(nèi)存相關的性能問題。這個問題最可怕的就是OutOfMemoryError:PermGen space錯誤。
這個錯誤通常會在以下三種情況下發(fā)生(服務器以Tomcat為例):
(1)應用程序加載了大量的類。
(2)在單一的Tomcat實例下運行多個Web應用程序。
(3)在運行的Tomcat實例中反復“熱部署”Web應用程序。
下面我們對該錯誤進行探究,分析該錯誤的常見原因,并給出避免這一錯誤的解決辦法。
1.JVM 的內(nèi)存結構
為了理解這一錯誤,我們必須了解JVM的內(nèi)存是如何構造的。
JVM有兩個內(nèi)存區(qū)域,一個是堆(heap),另一個是棧(stack)。局部的變量和方法駐留在棧里面,其余的一切都駐留在堆中。
Java的堆又進一步按照區(qū)域進行組織,這些區(qū)域被稱為generation。一個對象在JVM存在的時間越長,它被升遷到老的generation中的幾率就越高。年輕的generation要比老的generation更多地被垃圾回收。同時還存在一些單獨的堆空間,被稱為永久保存區(qū)域(permanent generation),它們不屬于Java堆的一部分,用來存放類和類的描述。
類加載器的工作是不斷地部署和取消部署Java類。例如,將一個Web應用程序部署到Web服務器或取消部署。在Web服務器上,所有應用程序都有自己的類加載器,部署或取消部署應用程序時,它的類定義和類加載器分別投入到永久保存區(qū)域中或者從永久保存區(qū)域中刪除。
2.OutOfMemoryError: PermGen space
當永久保存區(qū)域的空間耗盡時OutOfMemoryError: PermGen space就會發(fā)生,這個錯誤一般是由于內(nèi)存泄漏導致的。所謂內(nèi)存泄漏,是指java類和類加載器在被取消部署后不能被垃圾回收[2]。怎么會發(fā)生這種情況呢?舉個例子:假如我們有一個Student類,這個類是Web應用程序jar包的一部分,同時在Web服務器的lib文件夾中包含了某種日志框架,其中有一個Log類提供register方法調(diào)用,從而使得別的類通過注冊就可以使用日志功能。如果Student類被注冊了,那么Log類就開始擁有了一個對Student對象的引用(reference)。當Student類取消部署時,它仍然是注冊Log類的,Log類仍然擁有對Student對象的引用,因此,Student對象永遠不會被垃圾回收。此外,由于Student對象擁有一個對它的ClassLoader的引用,所以ClassLoader本身永遠也不會被垃圾回收,從而導致由它加載的所有類都不會被回收。
一個更為典型的例子是使用代理對象。Spring和Hibernate常常為某些類生成代理類,這些代理類也是通過類加載器加載的,并且存儲在永久保存區(qū)域的堆空間,它們永遠不會被丟棄,從而會導致永久保存區(qū)域的堆空間被填滿。
3.如何避免永久保存區(qū)域內(nèi)存不足
3.1 增加PermGen堆的最大尺寸
當遇到java.lang.OutOfMemoryError:PermGen space錯誤時,我們可以做的第一件事情是增加永久保存區(qū)域的最大尺寸,該尺寸的缺省設置是64 M,我們可以將它設置成128 M以上。這個工作不能通過常規(guī)的JVM參數(shù) -Xms(設置初始堆大?。┖?Xmx(設置最大堆大?。﹣硗瓿?,因為前面已經(jīng)提到,永久保存區(qū)域完全獨立于普通的Java堆,這些參數(shù)是用來設置普通的Java堆的。不過也有類似參數(shù)用于設置永久保存區(qū)域的規(guī)模:
java-XX:MaxPermSize=128 M
該設置將永久保存區(qū)域設置為128 M,這個大小是默認設置的兩倍。
對于Tomcat服務器,則需要修改TOMCAT_HOME/bin/catalina.sh
SET JAVA_OPTS=-XX:PermSize=64 M -XX:MaxPermSize=128 M [3]
3.2 避免使用靜態(tài)字段
確保在編寫Java類時,不要使用靜態(tài)變量作為對其他對象的引用。
3.3使用JDK動態(tài)代理,而不是CGLIB代理
一些第三方的框架,如CGLIB會吞食大量的PermGen。因此,當遇到PermGen錯誤時,應盡快升級cglib到最新版;改用JDK動態(tài)代理,也是一個不錯的選擇。
3.4更新到最新版本Hibernate3.2
此外,新版本的Hibernate不再使用CGLIB作為字節(jié)碼提供者了,所以及時升級Hibernate,會大大降低出錯的機會。
3.5共用的jar文件放到共享目錄下
如果在服務器上同時發(fā)布了多個應用,那么應該把共用的jar文件放到所有應用都可以訪問的目錄下。針對Tomcat而言,如果Tomcat下面有多個應用,應盡可能地把lib目錄下共用的jar文件放到Tomcat的common\lib或shared\lib下,以避免重復發(fā)布,發(fā)布速度和運行速度上也會有所提升。
4.結束語
內(nèi)存不足問題是潛伏較深的問題,且不容易解決。一般情況下,當發(fā)生該錯誤的時候,需要確定為什么某些類不被垃圾回收,只有這樣做才能夠消除這個錯誤。
參考文獻
[1] Jacquie Barker.Beginning Java Objects From Concept to Code[M] . Birmingham, UK. Published by Wrox Press Ltd.,2000.
[2]Oracle Docs, Chapter 3, Troubleshooting Memory Leaks,
http://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/memleaks.html.
[3] Eric Simmerman,Preventing Java's java.lang.OutOfMemoryError: PermGen space failure [EB],2006.