jdk1.7.0_79

這張圖我相信基本上對JVM有點接觸的都應該很熟悉,可以說這是JVM入門的第一課。其中的“堆”和“虛擬機棧(棧)”更是耳熟能詳。下面將圍繞這張圖對JVM的運行時數據區做一個簡單介紹。
程序計數器(Program Counter Register)
這和計算機操作系統中的程序計數器類似,在計算機操作系統中程序計數器表示這個進程要執行的下個指令的地址,對於JVM中的程序計數器可以看做是當前線程所執行的字節碼的行號指示器,每個線程都有一個程序計數器(這很好理解,每個線程都有在執行任務,如果線程切換后要能保證能恢復到正確的位置),重要的一點——程序計數器,這是JVM規范中唯一一個沒有規定會導致OutOfMemory(內存溢出,下文簡稱OOM)的區域。換句話上圖中的其余4個區域,都有可能導致OOM。
☆虛擬機棧(Java Virtual Machine Stacks)
這塊內存區域就是我們常常說的“棧”,我們所熟知的是它用於存放變量,也就是說例如:
int i = 0;
虛擬機棧內存就會用4個字節來存儲i變量。對於變量的內存空間是一開始就能確定的(對於引用型變量,它當然存儲的就是一個地址引用,其大小也是固定),所以這塊內存區域在編譯器就能夠確定下來,這塊區域可能會拋出StackOverflowError或者OOM錯誤。設置JVM參數”-Xss228k”(棧大小為228k)。
1 package com.jvm; 2 3 /** 4 * -Xss228k,虛擬機棧大小為228k 5 * Created by yulinfeng on 7/11/17. 6 */ 7 public class Test { 8 private static int count = 0; 9 10 public static void main(String[] args) { 11 Test test = new Test(); 12 test.test(); 13 } 14 15 /** 16 * 遞歸調用 17 */ 18 private void test() { 19 try { 20 count++; 21 test(); 22 } catch (Throwable e) { //Exception已經捕獲不了JVM拋出的StackOverflowError 23 System.out.println("遞歸調用次數" + count); 24 e.printStackTrace(); 25 } 26 } 27 }
這是一段沒有終止條件的遞歸,執行結果如下圖所示,JVM拋出StackOverflowError表示線程請求的棧深度大於JVM所允許的深度。

對於單線程情況下,無論如何拋出的都是StackOverflowError。如果要拋出OOM異常,導致的原因是不斷地在創建線程,直到將內存消耗殆盡。
JVM的內存由堆內存 + 方法區內存 + 剩余內存,也就是剩余內存=操作系統分配給JVM的內存 - 堆內存 - 方法區內存。-Xss設置的是每個線程的棧容量,也就是說可以創建的線程數量 = 剩余內存 / 棧內存。此時如果棧內存越大,可以創建的線程數量就少,就容易出現OOM;如果棧內存越小,可以創建的線程數量就多,就不容易出現OOM。
要避免這種情況最好就是減少堆內存+方法區內存,或者適當減少棧內存。對於棧內存的配置,一般采用默認值1M,或者采用64位操作系統以及64位的JVM。
本地方法棧(Native Method Stack)
本地方法棧和虛擬機棧類似,不同的是虛擬機棧服務的是Java方法,而本地方法棧服務的是Native方法。在HotSpot虛擬機實現中是把本地方法棧和虛擬機棧合二為一的,同理它也會拋出StackOverflowError和OOM異常。
☆Java堆(Java Heap)
對於堆,Java程序員都知道對象實例以及數組內存都要在堆上分配。堆不再被線程所獨有而是共享的一塊區域,它的確是用來存放對象實例,也是垃圾回收GC的主要區域。實際上它還能細分為:新生代(Young Generation)、老年代(Old Generation)。對於新生代又分為Eden空間、From Survivor空間、To Survivor空間。至於為什么這么分,這涉及JVM的垃圾回收機制,在這里不做敘述。堆同樣會拋出OOM異常,下面例子設置JVM參數” -Xms20M -Xmx20M“(前者表示初始堆大小20M,后者表示最大堆大小20M)。
1 package com.jvm; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * -Xms20M -Xmx20M 堆初始大小20M 堆最大大小20M 8 * Created by yulinfeng on 7/11/17. 9 */ 10 public class Test { 11 12 public static void main(String[] args) { 13 List<Test> list = new ArrayList<Test>(); 14 int count = 0; 15 try { 16 while (true) { 17 count++; 18 list.add(new Test()); //不斷創建線程 19 } 20 } catch (Throwable e) { 21 System.out.println("創建實例個數:" + count); 22 e.printStackTrace(); 23 } 24 25 } 26 }
執行的結果可以清楚地看到堆上的內存空間溢出了。

☆方法區(Method Area)
對於JVM的方法區,可能聽得最多的是另外一個說法——永久代(Permanent Generation),呼應堆的新生代和老年代。方法區和堆的划分是JVM規范的定義,而不同虛擬機有不同實現,對於Hotspot虛擬機來說,將方法區納入GC管理范圍,這樣就不必單獨管理方法區的內存,所以就有了”永久代“這么一說。方法區和操作系統進程的正文段(Text Segment)的作用非常類似,它存儲的是已被虛擬機加載的類信息、常量(從JDK7開始已經移至堆內存中)、靜態變量等數據。現設置JVM參數為”-XX:MaxPermSize=20M”(方法區最大內存為20M)。
1 package com.jvm; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 /** 7 * -XX:MaxPermSize=20M 方法區最大大小20M 8 * Created by yulinfeng on 7/11/17. 9 */ 10 public class Test { 11 12 public static void main(String[] args) { 13 List<String> list = new ArrayList<String>(); 14 int i = 0; 15 while (true) { 16 list.add(String.valueOf(i++).intern()); //不斷創建線程 17 } 18 } 19 }
實際上對於以上代碼,在JDK6、JDK7、JDK8運行結果均不一樣。原因就在於字符串常量池在JDK6的時候還是存放在方法區(永久代)所以它會拋出OutOfMemoryError:Permanent Space;而JDK7后則將字符串常量池移到了Java堆中,上面的代碼不會拋出OOM,若將堆內存改為20M則會拋出OutOfMemoryError:Java heap space;至於JDK8則是純粹取消了方法區這個概念,取而代之的是”元空間(Metaspace)“,所以在JDK8中虛擬機參數”-XX:MaxPermSize”也就沒有了任何意義,取代它的是”-XX:MetaspaceSize“和”-XX:MaxMetaspaceSize”等。
