Java中的OutOfMemoryError和JVM內存結構

轉自:http://hi.baidu.com/hwaspf/blog/item/d03cccd79f09323306088b28.html

OutOfMemoryError在開發過程中是司空見慣的,遇到這個錯誤,新手程序員都知道從兩個方面入手來解決:一是排查程序是否有BUG導致內存泄漏;二是調整JVM啓動參數增大內存。OutOfMemoryError有好幾種情況,每次遇到這個錯誤時,觀察OutOfMemoryError 後面的提示信息,就可以發現不同之處,如:

java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: unable to create new native thread
java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Requested array size exceeds VM limit

雖然都叫OutOfMemoryError,但每種錯誤背後的成因是不一樣的,解決方法也要視情況而定,不能一概而論。只有深入瞭解JVM的內存結構並仔細分析錯誤信息,纔有可能做到對症下藥,手到病除。

JVM規範

JVM規範對Java運行時的內存劃定了幾塊區域(詳見這裏),有:JVM棧(Java Virtual Machine Stacks)、堆(Heap)、方法區(Method Area)、常量池(Runtime Constant Pool)、本地方法棧(Native Method Stacks),但對各塊區域的內存佈局和地址空間卻沒有明確規定,而留給各JVM廠商發揮的空間。

HotSpot JVM

Sun自家的HotSpot JVM實現對堆內存結構有相對明確的說明。按照HotSpot JVM的實現,堆內存分爲3個代:Young Generation、Old(Tenured) Generation、Permanent Generation。衆所周知,GC(垃圾收集)就是發生在堆內存這三個代上面的。Young用於分配新的Java對象,其又被分爲三個部分:Eden Space和兩塊Survivor Space(稱爲From和To),Old用於存放在GC過程中從Young Gen中存活下來的對象,Permanent用於存放JVM加載的class等元數據。詳情參見HotSpot內存管理白皮書。堆的佈局圖示如下:

根據這些信息,我們可以推導出JVM規範的內存分區和HotSpot實現中內存區域的對應關係:JVM規範的Heap對應到Young和Old Generation,方法區和常量池對應到Permanent Generation。對於Stack內存,HotSpot實現也沒有詳細說明,但HotSpot白皮書上提到,Java線程棧是用宿主操作系統的棧和線程模型來表示的,Java方法和native方法共享相同的棧。因此,可以認爲在HotSpot中,JVM棧和本地方法棧是一回事。

操作系統

由於一個JVM進程首先是一個操作系統進程,因此會遵循操作系統進程地址空間的規定。32位系統的地址空間爲 4G,即最多表示4GB的虛擬內存。在Linux系統中,高地址的1G空間(即0xC0000000~0xFFFFFFFF)被系統內核佔用,低地址的 3G空間(即0×00000000~0xBFFFFFFF)爲用戶程序所使用(顯然JVM進程運行在這3G的地址空間中)。這3G的地址空間從低到高又分爲多個段;Text段用於存放程序二進制代碼;Data段用於存放編譯時已初始化的靜態變量;BSS段用於存放未初始化的靜態變量;Heap即堆,用於動態內存分配的數據結構,C語言的malloc函數申請的內存即是從此處分配的,Java的new實例化的對象也是自此分配。不同於前面三個段,Heap空間是可變的,其上界由低地址向高地址增長。內存映射區,加載的動態鏈接庫位於這個區中;Stack即棧空間,線程的執行即是佔用棧內存,棧空間也是可變的,但它是通過下界從高地址向低地址移動而增長的。詳情參見這裏。圖示如下:

JVM本身是由native code所編寫的,所以JVM進程同樣具有Text/Data/BSS/Heap/MemoryMapping/Stack等內存段。而Java語言的 Heap應當是建立在操作系統進程的Heap之上的,Java語言的Stack應該也是建立操作系統進程Stack之上的。 綜合HotSpot的內存區域和操作系統進程的地址空間,可以大致得到下列圖示:

Java線程的內存是位於JVM或操作系統的棧(Stack)空間中,不同於對象——是位於堆(Heap)中。這是很多新手程序員容易誤解的地方。注意,“Java線程的內存”這個用詞不是指Java.lang.Thread對象的內存,java.lang.Thread對象本身是在Heap中分配的,當調用start()方法之後,JVM會創建一個執行單元,最終會創建一個操作系統的native thread來執行,而這個執行單元或native thread是使用Stack內存空間的

經過上述鋪墊,可以得知,JVM進程的內存大致分爲Heap空間和Stack空間兩部分。Heap又分爲Young、Old、Permanent三個代。Stack分爲Java方法棧和native方法棧(不做區分),在Stack內存區中,可以創建多個線程棧,每個線程棧佔據Stack區中一小部分內存,線程棧是一個LIFO數據結構,每調用一個方法,會在棧頂創建一個Frame,方法返回時,相應的Frame會從棧頂移除(通過移動棧頂指針)。在這每一部分內存中,都有可能會出現溢出錯誤。回到開頭的OutOfMemoryError,下面逐個說明錯誤原因和解決方法(每個 OutOfMemoryError都有可能是程序BUG導致,因此解決方法不包括對BUG的排查)。

java.lang.OutOfMemoryError: Java heap space
原因:Heap內存溢出,意味着Young和Old generation的內存不夠。
解決:調整java啓動參數 -Xms -Xmx 來增加Heap內存。

java.lang.OutOfMemoryError: unable to create new native thread
原因:Stack空間不足以創建額外的線程,要麼是創建的線程過多,要麼是Stack空間確實小了。
解決:由於JVM沒有提供參數設置總的stack空間大小,但可以設置單個線程棧的大小;而系統的用戶空間一共是3G,除了Text/Data/BSS /MemoryMapping幾個段之外,Heap和Stack空間的總量有限,是此消彼長的。因此遇到這個錯誤,可以通過兩個途徑解決:1.通過 -Xss啓動參數減少單個線程棧大小,這樣便能開更多線程(當然不能太小,太小會出現StackOverflowError);2.通過-Xms -Xmx 兩參數減少Heap大小,將內存讓給Stack(前提是保證Heap空間夠用)。

java.lang.OutOfMemoryError: PermGen space
原因:Permanent Generation空間不足,不能加載額外的類。
解決:調整-XX:PermSize= -XX:MaxPermSize= 兩個參數來增大PermGen內存。一般情況下,這兩個參數不要手動設置,只要設置-Xmx足夠大即可,JVM會自行選擇合適的PermGen大小。

java.lang.OutOfMemoryError: Requested array size exceeds VM limit
原因:這個錯誤比較少見(試着new一個長度1億的數組看看),同樣是由於Heap空間不足。如果需要new一個如此之大的數組,程序邏輯多半是不合理的。
解決:修改程序邏輯吧。或者也可以通過-Xmx來增大堆內存。

在GC花費了大量時間,卻僅回收了少量內存時,也會報出OutOfMemoryError,我只遇到過一兩次。當使用-XX:+UseParallelGC或-XX:+UseConcMarkSweepGC收集器時,在上述情況下會報錯,在HotSpot GC Turning文檔上有說明:
The parallel(concurrent) collector will throw an OutOfMemoryError if too much time is being spent in garbage collection: if more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, an OutOfMemoryError will be thrown.
對這個問題,一是需要進行GC turning,二是需要優化程序邏輯。

java.lang.StackOverflowError
原因:這也內存溢出錯誤的一種,即線程棧的溢出,要麼是方法調用層次過多(比如存在無限遞歸調用),要麼是線程棧太小。
解決:優化程序設計,減少方法調用層次;調整-Xss參數增加線程棧大小。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章