虛擬機堆疊是JVM記憶體結構中執行緒私有的模塊之一,特性是先進后出,這個特性決定了方法的呼叫程序都在堆疊中進行,每呼叫一個方法在堆疊中就會生成一個此方法對應的堆疊幀,堆疊幀中包括四個部磁區域變數表、運算元堆疊、動態連接和方法回傳地址,當一個堆疊的記憶體不足以容納足夠的堆疊幀時,即所謂的堆疊嘗試大于虛擬所允許的深度時就會拋出StackOverFlowError例外,當對堆疊進行擴展遇到JVM記憶體不足時會拋出OutOfMemoryError例外,接下來通過圖形和具體的代碼對堆疊進行介紹
堆疊結構
堆疊的結構如下圖所示:

其中Method1和Method2對應的是兩個堆疊幀,從圖中可知Method2中呼叫了Method1,每個堆疊幀中都有4個核心部分,分別為區域變數表、運算元堆疊、動態鏈接以及方法回傳地址,當然也有其他部分,這里不作介紹,
區域變數表
見名知意其用于存放局變數的,即一個方法中的所有變數都存放在這種表里,在運行時階段會針對每一個變數進行賦值,
運算元堆疊
用于存放方法中所有的基礎型別變數的值,即區域變數表中變數所對應的具體的值,
動態鏈接
用于將常量池中指向某呼叫方法的符號參考轉換為該呼叫方法的直接參考,
方法回傳地址
可以理解成用于存放呼叫此方法的位置所在行號,例如:Method2 在第7行呼叫了Method1,那么Method1的堆疊幀中對應的方法回傳地址就是7,當然可以這么去理解,但其實存放的是一段地址,
代碼分析
通過上面的介紹,雖然有一個大致的了解,但可能還有一些模糊,接下來通過一段代碼來分析,
public class JVMTest {
public int add(){
int a = 3;
int b = 10;
int c = a+b;
return c;
}
public static void main(String[] args) {
JVMTest jvmTest = new JVMTest();
int result = jvmTest.add();
System.out.println("The result is : "+result);
}
}
這段代碼很簡單,一個類中兩個方法,main 和 add 方法,其中在main方法中呼叫了add 方法,add方法執行了一個簡單的加法運算,那么這段代碼在運行時虛擬機堆疊到底是如何運作的,這里通過分析一下這段代碼的位元組碼檔案,位元組碼檔案可以通過javap 命令翻譯成可讀的指令碼,本代碼通過 javap -c JVMTest.class 得到如下結果:
Compiled from "JVMTest.java"
public class com.research.JVMTest {
public com.research.JVMTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int add();
Code:
0: iconst_3
1: istore_1
2: bipush 10
4: istore_2
5: iload_1
6: iload_2
7: iadd
8: istore_3
9: iload_3
10: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/research/JVMTest
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method add:()I
12: istore_2
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_2
17: invokedynamic #6, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
22: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
}
看起來很頭疼,慢慢來不著急,這里主要通過add方法和main方法來分析堆疊里四個核心區域是如何作業的,在分析前我們得讀懂其中的指令,沒必要記,讀的時候通過官方的jvm指令手冊查詢即可,這里我將add方法中的10條指令整理如下:
| 指令 | 作用 | 作用域 |
|---|---|---|
| iconst_3 | int 型常量值3進運算元堆疊 | 運算元堆疊 |
| istore_1 | 將堆疊頂int型數值存入第一個區域變數 | 從運算元堆疊到區域變數表 |
| bipush | 將單位元組的常量值(-128~127)推送至堆疊頂 | 運算元堆疊 |
| istore_2 | 將堆疊頂int型數值存入第二個本地變數 | 從運算元堆疊到區域變數表 |
| iload_1 | 將第一個int型本地變數推送至運算元堆疊頂 | 從區域變數表到運算元堆疊 |
| iload_2 | 將第二個int型本地變數推送至運算元堆疊頂 | 從區域變數表到運算元堆疊 |
| iadd | 將堆疊頂兩int型數值相加并將結果壓入運算元堆疊頂 | 運算元堆疊 |
| istore_3 | 將堆疊頂int型數值存入第三個本地變數 | 從運算元堆疊到區域變數表 |
| iload_3 | 將第三個int型本地變數推送至運算元堆疊頂 | 從區域變數表到運算元堆疊 |
| ireturn | 從當前方法回傳int | 運算元堆疊 |
PS: 大學學過匯編的同學看到這里應該會覺得這些指令似曾相識,當然我也是這么覺得的
整個add方法的執行程序如下圖所示,每一大塊代表一條指令,指令中顯示了堆疊幀中各個部分的動態變化,其中區域變數表和運算元堆疊都對應著一塊記憶體,運算元堆疊說白了就是一個暫存區,暫存變數和計算的中結果,方法回傳地址作為此方法的出口,

不難發現add方法中定義的都是基本型別變數,如果定義的是參考型別變數,區域變數表中會是一種什么情況,看向main方法:
main方法中定義了一個參考型別的區域變數,它的值是一個指向物件的地址,當然這個物件不是存放在運算元堆疊中,而是存放在堆中(由淺入深了解JVM-堆),寫到這里可以在虛擬機堆疊和堆之間建立一座橋了:

快看到光明了,剩下最后一個核心部分:動態鏈接,既然說是動態鏈接,它是怎么個動態法,
在Java源檔案被編譯到位元組碼檔案中時,所有的變數和方法參考都作為符號參考( symbolic Reference)保存在class檔案的常量池(方法區)里, 比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號參考來表示的(上文位元組碼main方法中:17: invokedynamic #6, 0 ),那么動態鏈接的作用就是為了將這些符號參考轉換為呼叫方法的直接參考,
至此,虛擬堆疊的作業原理介紹完了,至于其他部分的作業原理請參考如下博文
由淺入深了解JVM-記憶體結構
由淺入深了解JVM-堆
由淺入深了解JVM-程式計數器
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/262548.html
標籤:java
上一篇:Java面向物件(知識點整理)
