這張圖我相信基本上對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抛出的StackOverflowError23 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”等。
以上是JVM基礎入門之運行時資料區的詳細內容。更多資訊請關注PHP中文網其他相關文章!