這張圖我相信基本上對JVM有點接觸的都應該很熟悉,可以說這是JVM入門的第一課。其中的“堆”和“虛擬機棧(棧)”更是耳熟能詳。下麵將圍繞這張圖對JVM的運行時數據區做一個簡單介紹。 程式計數器(Program Counter Register) 這和電腦操作系統中的程式計數器類似,在電腦操作系統 ...
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”等。