Java內存區域與內存溢出
內存區域
Java虛擬機在執行Java程序的過程中會把他所管理的內存劃分為若干個不同的數據區域。Java虛擬機規范將JVM所管理的內存分為以下幾個運行時數據區:程序計數器、Java虛擬機棧、本地方法棧、Java堆、方法區。下面詳細闡述各數據區所存儲的數據類型。
程序計數器(Program Counter Register)
一塊較小的內存空間,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時通過改變該計數器的值來選擇下一條需要執行的字節碼指令,分支、跳轉、循環等基礎功能都要依賴它來實現。每條線程都有一個獨立的的程序計數器,各線程間的計數器互不影響,因此該區域是線程私有的。
當線程在執行一個Java方法時,該計數器記錄的是正在執行的虛擬機字節碼指令的地址,當線程在執行的是Native方法(調用本地操作系統方法)時,該計數器的值為空。另外,該內存區域是唯一一個在Java虛擬機規范中么有規定任何OOM(內存溢出:OutOfMemoryError)情況的區域。
Java虛擬機棧(Java Virtual Machine Stacks)
該區域也是線程私有的,它的生命周期也與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀,棧它是用于支持續虛擬機進行方法調用和方法執行的數據結構。對于執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。棧幀用于存儲局部變量表、操作數棧、動態鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時,棧幀中需要多大的局部變量表、多深的操作數棧都已經完全確定了,并且寫入了方法表的Code屬性之中。因此,一個棧幀需要分配多少內存,不會受到程序運行期變量數據的影響,而僅僅取決于具體的虛擬機實現。
在Java虛擬機規范中,對這個區域規定了兩種異常情況:
1、如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常。
2、如果虛擬機在動態擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
這里有一點要重點說明,在多線程情況下,給每個線程的棧分配的內存越大,越容易產生內存溢出異常。操作系統為每個進程分配的內存是有限制的,虛擬機提供了參數來控制Java堆和方法區這兩部分內存的最大值,忽略掉程序計數器消耗的內存(很小),以及進程本身消耗的內存,剩下的內存便給了虛擬機棧和本地方法棧,每個線程分配到的棧容量越大,可以建立的線程數量自然就越少。因此,如果是建立過多的線程導致的內存溢出,在不能減少線程數的情況下,就只能通過減少最大堆和每個線程的棧容量來換取更多的線程。當由于創建過量線程發生OOM時,會報錯:java.lang.OutOfMemoryError, unable to create new native thread。
本地方法棧(Native Method Stacks)
該區域與虛擬機棧所發揮的作用非常相似,只是虛擬機棧為虛擬機執行Java方法服務,而本地方法棧則為使用到的本地操作系統(Native)方法服務。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError與OutOfMemoryError異常。
Java堆(Java Heap)
Java Heap是Java虛擬機所管理的內存中最大的一塊,它是所有線程共享的一塊內存區域,幾乎所有的對象實例和數組都在這類分配內存。Java Heap是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。
根據Java虛擬機規范的規定,Java堆可以處在物理上不連續的內存空間中,只要邏輯上是連續的即可。如果在堆中沒有內存可分配時,并且堆也無法擴展時,將會拋出OutOfMemoryError異常。
注意:隨著JIT編譯器的發展與逃逸技術逐漸成熟,所有對象都分配在堆上也逐漸變得不是那么絕對了,線程共享的Java堆中也可能劃分出線程私有的分配緩沖區(TLAB)。
方法區(Method Area)
方法區也是各個線程共享的內存區域,它用于存儲已經被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。Java虛擬機規范把方法區描述為Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的內存,可以選擇固定大小或可擴展,另外,虛擬機規范允許該區域可以選擇不實現垃圾回收。相對而言,垃圾收集行為在這個區域比較少出現。不過,這部分區域的回收是有必要的,如果這部分區域永遠不回收,那么類型就無法卸載,我們就無法加載更多的類,HotSpot的該區域有實現垃圾回收。
根據Java虛擬機規范的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。
直接內存(Direct Memory)
直接內存并不是虛擬機運行時數據區的一部分,也不是Java虛擬機規范中定義的內存區域,它直接從操作系統中分配,因此不受Java堆大小的限制,但是會受到本機總內存的大小及處理器尋址空間的限制,因此它也可能導致OutOfMemoryError異常出現。在JDK1.4中新引入了NIO機制,它是一種基于通道與緩沖區的新I/O方式,可以直接從操作系統中分配直接內存,即在堆外分配內存,這樣能在一些場景中提高性能,因為避免了在Java堆和Native堆中來回復制數據。
當使用超過虛擬機允許的直接內存時,虛擬機會拋出OutOfMemoryError異常,由DirectMemory導致的內存溢出,一個明顯的特征是在Heap Dump文件中不會看見明顯的異常。一般來說,如果發現OOM后Dump文件很小,那就應該考慮一下,是不是這塊內存發生了溢出。
內存溢出
Java堆內存溢出
public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } } |
運行以上代碼時,可以增加運行參數-Xms20m -Xmx20m,該參數限制Java堆大小為20M,不可擴展。運行結果如下:
<span style="color: #ff0000;">Exception in thread "main" java.lang.OutOfMemoryError: Java heap space</span> at java.util.Arrays.copyOf(Arrays.java:2245) at java.util.Arrays.copyOf(Arrays.java:2219) at java.util.ArrayList.grow(ArrayList.java:242) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) at java.util.ArrayList.add(ArrayList.java:440) at HeapOOM.main(HeapOOM.java:17) |
可以看到,在堆內存溢出時,除了會報錯java.lang.OutOfMemoryError外,還會跟著進一步提示Java heap space。
虛擬機棧和本地方法棧溢出
要讓虛擬機棧內存溢出,我們可以使用遞歸調用:因為每次方法調用都需要向棧中壓入調用信息,當棧的大小固定時,過深的遞歸將向棧中壓入過量信息,導致
StackOverflowError: public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } } |
運行以上代碼,輸出如下:
stack length:10828
<span style="color: #ff0000;">Exception in thread "main" java.lang.StackOverflowError</span>
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:10)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
at JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
可以看到,在我的電腦上運行以上代碼,最多支持的棧深度是10828層,當發生棧溢出時,會報錯java.lang.StackOverflowError。
方法區溢出
方法區用于存放Class的相關信息,如類名、訪問修飾符、字段描述等,對于這個區域的測試,基本思路是運行時使用CGLib產生大量的類去填充方法區,直到溢出:
public class JavaMethodAreaOOM { static class OOMObject { } public static void main(String[] args) { while( true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject. class); enhancer.setUseCache( false); enhancer.setCallback( new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } } |
運行時增加虛擬機參數:-XX:PermSize=10M -XX:MaxPermSize=10M,限制永久代大小為10M,最后報錯為java.lang.OutOfMemoryError: PermGen space。報錯信息明確說明,溢出區域為永久代。
總結
本文主要說明Java虛擬機一共分為哪幾塊內存區域,以及這幾塊內存區域是否會內存溢出,如果這些區域發生內存溢出報錯如何。了解這些知識后,以后遇到內存溢出報錯,我們就可以定位到具體內存區域,然后具體問題,具體分析。
posted on 2014-10-30 11:50 順其自然EVO 閱讀(206) 評論(0) 編輯 收藏 所屬分類: 測試學習專欄