3.2 棧幀組成之操作數棧
操作數棧是棧幀的主要內容之一,它主要用於保存計算過程中的中間結果,同時作為計算過程中變量臨時的存儲空間。
操作數棧也是一個先進后出的數據結構,只支持入棧和出棧兩種操作,許多java字節碼指令都需要通過操作數棧進行參數傳遞。比如add指令,它就會在操作數棧中彈出兩個整數並進行加法計算,計算結果會被入棧,如圖:顯示了iadd前后操作數棧的變化。
3.3 幀數據區
除了局部變量表和操作數棧,java棧幀還需要一些數據來支持常量池的解析、正常方法返回和異常處理等。大部分java字節碼指令需要進行常量池訪問,在幀數據區中保留着訪問常量池的指針,方便程序訪問常量池。
此外,當函數返回或者出現異常時,虛擬機必須恢復調用者函數的棧幀,並讓調用者函數繼續執行下去。對於異常處理,虛擬機必須有一個異常處理表,方便在發生異常時找到處理異常的代碼,因此異常處理表也是幀數據區中重要的一部分,一個典型的異常處理表如下所示:
Exception table:
from to target type
4 16 19 any
19 21 19 any
它表示在字節碼偏移量4--16字節可能拋出任意異常,如果拋出異常,則跳轉到字節碼偏移量19處執行。當方法拋出異常時,虛擬機就會查找類似的異常表來處理,如果無法在異常表中找到合適的處理方法,則會結束當前函數調用,返回調用函數,並在調用函數中拋出相同的異常,並查找調用函數的異常表來進行處理。
3.4 棧上分配
棧上分配是java虛擬機提供的一項優化技術,它的基本思想是,對於那些線程私有的對象(這里指不可能被其他線程訪問的對象),可以將他們打散分配到棧上,而不是分配到堆上。分配到棧上的好處是可以在函數調用結束后自行銷毀,而不需要垃圾回收器的介入,從而提高系統的性能。
棧上分配的一個技術基礎是進行逃逸分析,逃逸分析的目的是判斷對象的作用域是否有可能逃逸出函數體。如下代碼所示顯示了一個逃逸對象:
private static User u;
public static void alloc(){
u = new User();
u.id = 5;
u.name = "jim";
}
對象u是類的成員變量,該字段有可能被任何線程訪問,因此屬於逃逸對象,而以下對象顯示了一個非逃逸對象:
public static void alloc(){
User u = new User();
u.id = 5;
u.name = "jim";
}
在上述代碼中,對象User u 以局部變量的形式存在,並且該對象並沒有被alloc()函數返回或者出現任何形式的公開,因此它未發生逃逸,所以對於這種情況,虛擬機就有可能將User u 分配在棧上,而不是在堆上。
對於大量的零散小對象,棧上分配提供了一種良好的對象分配優化策略,棧上分配速度快,並且可以有效避免垃圾回收帶來的負面影響。但由於棧和堆空間相比,棧空間較小,因此對於大對象無法也不適合在棧上分配。
實例1:測試非逃逸對象的分配空間位置
package com.jvm;
public class OnStackTest {
public static class User{
public int id = 0;
public String name = "";
}
public static void alloc(){
User u = new User();
u.id = 5;
u.name = "jim";
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for(int i=0;i<100000000;i++){
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e-b);
}
}
使用-Xmx10M -XX:+PrintGC 虛擬機參數運行代碼:
[GC (Allocation Failure) 2048K->544K(9728K), 0.0015011 secs]
10
上述代碼在主函數中進行了1億次alloc()調用進行對象的創建,由於User對象實例需要占用約16byte的空間,因此累計分配空間將達到1.5G,如果堆空間小於這個值,就必然發生GC。而此時我們只分配了最大的堆內存為10M,如果這些對象在堆上創建,必然會引起大量的垃圾回收現象,查看垃圾回收日志,並沒有。所以,說明其對象分配在棧上。
實例2:對比測試逃逸對象的分配空間位置:
package com.jvm;
public class OnStackTest {
public static class User{
public int id = 0;
public String name = "";
}
public static User u;
public static void alloc(){
u = new User();
u.id = 5;
u.name = "jim";
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for(int i=0;i<100000000;i++){
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e-b);
}
}
同樣使用虛擬機參數-Xmx10M -XX:+PrintGC設置最大堆空間和打印垃圾回收日志,運行此代碼:
可見,發生大量的垃圾回收現象,說明此時堆內存遠遠不夠,需要不斷的進行垃圾回收。
4 方法區
和堆一樣,方法區是一塊所有線程共享的內存區域,它用於保存系統的類信息,比如類的字段、方法、常量池等。方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,導致方法區的溢出,虛擬機同樣會拋出內存溢出錯誤。
在JDK1.6、JDK1.7中,方法區可以理解為永久區(Perm)。永久區可以使用參數-XX:PermSize和-XX:MaxPermSize指定,默認情況下,-XX:MaxPermSize為64M。一個大的永久區可以保存更多的類信息。如果系統使用了一些動態代理,那么有可能會在運行時生成大量的類,如果這樣,就需要設置一個合理的永久區大小,確保不發生永久區內存溢出。
在JDK1.8中,永久區已經被徹底移除,取而代之的是元數據區,元數據區大小可以使用參數-XX:MaxMetaspaceSize指定(一個大的元數據區可以使系統支持更多的類),這是一塊堆外的直接內存。與永久區不同,如果不指定大小,默認情況下,虛擬機會耗盡所有的可用系統內存。
如果元數據區發生異常,虛擬機一樣會拋出異常。