1. Java虛擬機棧(Java棧)
🌳 虛擬機棧也稱為Java棧,每個方法被執行的時候,Java虛擬機都會同步創建一個棧幀(Stack Frame)。
- Java虛擬機棧是線程私有的,它的生命周期與線程相同(隨線程而生,隨線程而滅)。
- 棧幀包括局部變量表、操作數棧、動態鏈接、方法返回地址和一些附加信息。
- 每一個方法被調用直至執行完畢的過程,就對應這一個棧幀在虛擬機棧中從入棧到出棧的過程。
虛擬機棧示意圖如下所示:
2. 虛擬機棧大小的調整
Java虛擬機規范允許虛擬機棧的大小固定不變或者動態擴展。
- 固定情況下:如果線程請求分配的棧容量超過Java虛擬機允許的最大容量,則拋出StackOverflowError異常;
- 可動態擴展情況下:嘗試擴展的時候無法申請到足夠的內存;或者在創建新的線程的時候沒有足夠的內存去創建對應的虛擬機棧,則會拋出OutOfMemoryError異常。
可以通過 java -Xss<size>
設置 Java 線程堆棧大小,或者在idea中 help -> edit vm option中改變大小
3. 運行時棧幀結構
每個棧幀包含5個組成部分:局部變量表、操作數棧(Operand Stack)、動態鏈接(Dynamic Linking)、方法返回地址(Return Address)和一些附加信息
3. 1 局部變量表
局部變量表(Local Variables)是一組變量值的存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯為Class文件時,就在方法的Code屬性的max_locals數據項中確定了方法所需的分配的局部變量表的最大容量。
🌳局部變量表的容量
局部變量表中32位以內的數據類型(除long和double外)只占用一個slot,64位類型(long和double)占用兩個slot。舉個例子:
public class Test {
public static void hello(String name) {
Date date = new Date();
long number = 200L;
double salary = 6000.0;
int count = 1;
}
}
查看字節碼文件有👇 (還可以在idea中使用jclasslib插件來查看局部變量表信息,使用:build project -> view -> show binaryCode with jclasslib)
根據上圖可以看出:String和Date引用類型(reference)分別占用一個slot(第0個和第1個)、long類型的變量占用第2個和第3個slot、double類型的變量占用第4個和第5個slot、而int類型的變量則占用第6個slot。
🍁 如果執行沒有被static修飾的方法,那么局部變量表中第0位索引的變量槽,默認是用於傳遞方法所屬對象實例的引用,也就是this(當前實例對象的引用)
public class Test {
public void halo(String name) {
Date date = new Date();
int count = 1;
}
}
可以看到,非靜態方法的局部變量表首位就存放了this對象,這也是靜態方法內無法使用this的原因(因為靜態方法的局部變量表中沒有this對象)。
🌳 局部變量表容量的大小
在編譯器就可以唯一確定下來,並保存在方法的Code屬性的maximum locacl variables數據項中,就拿上面Test類的hello方法來說,其字節碼里已經指明了局部變量表的大小:
public static void hello(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=7, args_size=1 // locals = 7 就是局部變量表的大小
0: new #2 // class java/util/Date
3: dup
4: invokespecial #3 // Method java/util/Date."<init>":()V
7: astore_1
8: ldc2_w #4 // long 200l
11: lstore_2
12: ldc2_w #6 // double 6000.0d
15: dstore 4
17: iconst_1
18: istore 6
20: return
使用jclasslib插件查看局部變量表信息
⚠️方法的調用
✅方法調用並不等同於方法中的代碼被執行了。方法調用階段唯一的任務就是確定被調用方法的版本(即調用哪個方法),暫時未涉及方法內部的執行邏輯(具體運行過程)。
所有方法調用的目標方法都是一個常量池中的符號引用,而不是方法在實際運行時內存布局中的入口地址(即非直接引用)。
在類加載的解析階段,會將其中一部分符號引用轉化為直接飲用,能夠解析的前提就是方法在程序真正運行前就有一個可以確定的調用版本,並且這個方法的調用版本在運行期間是不可改變的。
也就是說:調用目標在程序代碼寫好、編譯器進行編譯那一刻就確定下來了。這類方法的調用被稱為解析
🌳 查看局部變量的作用范圍
public class Test {
public static void hello(String s){
int count = 1;
}
}
public static void hello(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: iconst_1
1: istore_1
2: return
LineNumberTable:
line 10: 0
line 11: 2
參考局表變量表信息:根據字節碼PC計數器知,s 的起始指令地址為0,則 s 在方法開始時就生效,查看LineNumberTable -> line 10: 0可知,對應的代碼在第10行,s 的作用域長度為 3 ,即字節碼指令 0 - 2 范圍內有效,也就是整個hello方法內都有效。
🌳變量槽的重復使用
當方法體中定義的變量,其作用域並不一定會覆蓋整個整個方法體,如果當前字節碼PC計數器的值已經超出了某個變量的作用域,那么這個變量對應的變量槽就可以交給其他變量來重用。
3. 2 操作數棧
🍀 操作數棧(Operand Stack)也常被成為操作棧,是一個后入先出棧,用於保存計算過程中的中間結果,同時作為計算過程中變量臨時的存儲空間。其最大深度在編譯時就被寫到了Code屬性的max_stacks中。
public class Test {
public int add() {
int a = 1;
int b = 1;
int c = a + b;
return c;
}
}
查看.class文件
🍀 棧中的任何一個元素都可以是任意的Java數據類型,32bit的類型占用一個棧深度,64bit的類型占用兩個棧單位深度:
// 64bit數據類型有 Long和Double
// 32bit數據類型是除了 Long和Double
public class Test {
public void test() {
Long a = 1L;
}
}
查看.class文件
🍀操組數棧在方法運行時的具體執行過程
操作數棧在方法的執行過程中,根據字節碼指令往棧中寫入數據或提取數據,即入棧和出棧操作。雖然棧是用數組實現的,但根據棧的特性,對棧中數據訪問不能通過索引,而是只能通過標准的入棧和出棧操作來完成一次數據訪問。
下面通過一個例子來感受PC寄存器,局部變量表和操作數棧是如何相互配合完成一次方法的執行,代碼如下所示:
public class Test {
public void add() {
int a = 15;
int b = 1;
int c = a + b;
}
}
在查看字節碼指令之前,先記錄下幾個入棧出棧的字節碼指令含義:
- 當int取值 -1 ~ 5 采用iconst指令入棧;
- 取值 -128 ~ 127(byte有效范圍)采用bipush指令入棧;
- 取值 -32768 ~ 32767(short有效范圍)采用sipush指令入棧;
- 取值 -2147483648 ~ 2147483647(int有效范圍)采用ldc指令入棧;
- istore,棧頂元素出棧,保存到局部變量表中;
- iload,從局部變量表中加載數據入棧。
指令執行過程中,PC寄存器,局部變量表和操作數棧狀態如下圖所示👇
如果被調用的方法帶有返回值的話,其返回值會被壓入當前棧幀的操作數棧中。
3. 3 動態鏈接
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態鏈接(Dynamic Linking)。Class 文件的常量池中存在大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用作為參數,這些符號引用一部分會在類加載階段或第一次使用時轉化為直接引用,這種轉化成為靜態解析。另一部分將在每一次運行期間轉化為直接引用,這部分稱為動態連接。
3. 4 方法返回地址
當一個方法開始執行后,只有兩種方式可以退出這個方法。
一種是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層方法的調用者,是否有返回值和返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口。
另一種退出方式是,在方法執行過程中遇到了異常,並且這個異常沒有在方法體內得到處理,無論是 Java 虛擬機內部產生的異常,還是代碼中使用 athrow 字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導致方法退出。這種稱為異常完成出口。一個方法使用異常完成出口的方式退出,是不會給上層調用者產生任何返回值的。
無論采用何種退出方式,在方法退出后都需要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能需要在棧幀中保存一些信息,用來恢復它的上層方法的執行狀態。一般來說,方法正常退出時,調用者的 PC 計數器的值可以作為返回地址,棧幀中很可能會保存這個計數器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。
方法退出的過程實際上就等同於把當前棧幀出棧,因此退出時可能執行的操作有:恢復上次方法的局部變量表和操作數棧,把返回值(如果有的話)壓入調用者棧幀的操作數棧中,調整 PC 計數器的值以指向方法調用指令后面的一條指令等。
3.5 附加信息
虛擬機規范允許具體的虛擬機實現增加一些規范里沒有描述的信息到棧幀中,例如與調試相關的信息,這部分信息完全取決於具體的虛擬機實現。實際開發中,一般會把動態連接、方法返回地址與其他附加信息全部歸為一類,成為棧幀信息。
本博文以及圖片部分借鑒於:https://mrbird.cc/JVM-Learn.html,感謝!!!