第 5 章 虛擬機堆疊
1、虛擬機堆疊概述
微信搜一搜: 全堆疊小劉,獲取文章全套 pdf版
1.1、虛擬機堆疊的出現背景
檔案網址
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
虛擬機堆疊出現的背景
- 由于跨平臺性的設計,Java的指令都是根據堆疊來設計的,不同平臺CPU架構不同,所以不能設計為基于暫存器的,
- 優點是跨平臺,指令集小,編譯器容易實作,缺點是性能下降,實作同樣的功能需要更多的指令,
記憶體中的堆疊與堆
首先堆疊是運行時的單位,而堆是存盤的單位
- 堆疊解決程式的運行問題,即程式如何執行,或者說如何處理資料,
- 堆解決的是資料存盤的問題,即資料怎么放,放哪里

1.2、虛擬機堆疊的存盤內容
虛擬機堆疊的基本內容
Java虛擬機堆疊是什么?
Java虛擬機堆疊(Java Virtual Machine Stack),早期也叫Java堆疊,每個執行緒在創建時都會創建一個虛擬機堆疊,其內部保存一個個的堆疊幀(Stack Frame), 對應著一次次的Java方法呼叫,堆疊是執行緒私有的
虛擬機堆疊的生命周期
生命周期和執行緒一致,也就是執行緒結束了,該虛擬機堆疊也銷毀了
虛擬機堆疊的作用
主管Java程式的運行,它 保存方法的區域變數(8 種基本資料型別、物件的參考地址)、部分結果,并參與方法的呼叫和回傳,
- 區域變數,它是相比于成員變數來說的(或屬性)
- 基本資料型別變數 VS 參考型別變數(類、陣列、介面)
1.3、虛擬機堆疊的特點
堆疊的特點
堆疊是一種快速有效的分配存盤方式,訪問速度僅次于程式計數器,JVM直接對Java堆疊的操作只有兩個:
- 每個方法執行,伴隨著 進堆疊(入堆疊、壓堆疊)
- 執行結束后的 出堆疊作業
對于堆疊來說不存在垃圾回收問題(堆疊存在溢位的情況)

1.4、虛擬機堆疊的例外
堆疊中可能出現的例外
面試題:堆疊中可能出現的例外
- Java 虛擬機規范允許Java堆疊的大小 是動態的或者是固定不變的,
- 如果采用固定大小的Java虛擬機堆疊,那每一個執行緒的Java虛擬機堆疊容量可以在執行緒創建的時候獨立選定,
- 如果執行緒請求分配的堆疊容量超過Java虛擬機堆疊允許的最大容量,Java虛擬機將會拋出一個 StackoverflowError 例外,
- 如果Java虛擬機堆疊可以動態擴展,并且在嘗試擴展的時候無法申請到足夠的記憶體,或者在創建新的執行緒時沒有足夠的記憶體去創建對應的虛擬機堆疊,那Java虛擬機將會拋出一個 OutofMemoryError 例外,
堆疊例外演示
- 代碼
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
- 遞回呼叫 11418 次后,出現堆疊記憶體溢位

1.5、設定堆疊記憶體大小
設定堆疊記憶體的大小
- 我們可以使用引數 -Xss 選項來設定執行緒的最大堆疊空間,堆疊的大小直接決定了函式呼叫的最大可達深度,
-Xss1024m // 栈内存为 1024MBS
-Xss1024k // 栈内存为 1024KB
- 設定執行緒的最大堆疊空間:256KB

- 代碼測驗
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
- 遞回 2471 次,堆疊記憶體溢位

2、堆疊的存盤單位
2.1、堆疊的運行原理
堆疊存盤什么?
- 每個執行緒都有自己的堆疊,堆疊中的資料都是以 堆疊幀(Stack Frame)的格式存在
- 在這個執行緒上 正在執行的每個方法都各自對應一個堆疊幀(Stack Frame),
- 堆疊幀是一個記憶體區塊,是一個資料集,維系著 方法執行程序中的各種資料資訊,
堆疊的運行原理
- JVM直接對Java堆疊的操作只有兩個,就是對堆疊幀的 壓堆疊和出堆疊,遵循先進后出(后進先出)原則
- 在一潭訓動執行緒中,一個時間點上,只會有一個活動的堆疊幀,即 只有當前正在執行的方法的堆疊幀(堆疊頂堆疊幀)是有效的
- 這個堆疊幀被稱為 當前堆疊幀(Current Frame)
- 與當前堆疊幀相對應的方法就是 當前方法(Current Method)
- 定義這個方法的類就是 當前類(Current Class)
- 執行引擎運行的所有位元組碼指令只針對當前堆疊幀進行操作,
- 如果在該方法中呼叫了其他方法,對應的新的堆疊幀會被創建出來,放在堆疊的頂端,成為新的當前幀,
- 不同執行緒中所包含的堆疊幀是不允許存在相互參考的,即不可能在一個堆疊幀之中參考另外一個執行緒的堆疊幀,
- 如果當前方法呼叫了其他方法, 方法回傳之際,當前堆疊幀會傳回此方法的執行結果給前一個堆疊幀,接著,虛擬機會丟棄當前堆疊幀,使得前一個堆疊幀重新成為當前堆疊幀,
- Java方法有兩種回傳函式的方式,但不管使用哪種方式,都會導致堆疊幀被彈出
- 一種是 正常的函式回傳,使用return指令
- 另外一種是 拋出例外

代碼示例:
- 代碼
public class StackFrameTest {
public static void main(String[] args) {
StackFrameTest test = new StackFrameTest();
test.method1();
}
public void method1() {
System.out.println("method1()開始執行...");
method2();
System.out.println("method1()執行結束...");
}
public int method2() {
System.out.println("method2()開始執行...");
int i = 10;
int m = (int) method3();
System.out.println("method2()即將結束...");
return i + m;
}
public double method3() {
System.out.println("method3()開始執行...");
double j = 20.0;
System.out.println("method3()即將結束...");
return j;
}
}
- 先執行的函式,最后執行結束
method1()开始执行...
method2()开始执行...
method3()开始执行...
method3()即将结束...
method2()即将结束...
method1()执行结束...
- 反編譯,可以看到每個方法后面都帶有 return 陳述句或者 ireturn 陳述句
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String method1()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #8 // Method method2:()I
12: pop
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: ldc #9 // String method1()执行结束...
18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
21: return
LineNumberTable:
line 16: 0
line 17: 8
line 18: 13
line 19: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/atguigu/java1/StackFrameTest;
public int method2();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String method2()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: bipush 10
10: istore_1
11: aload_0
12: invokevirtual #11 // Method method3:()D
15: d2i
16: istore_2
17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #12 // String method2()即将结束...
22: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: iload_1
26: iload_2
27: iadd
28: ireturn
LineNumberTable:
line 22: 0
line 23: 8
line 24: 11
line 25: 17
line 26: 25
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/atguigu/java1/StackFrameTest;
11 18 1 i I
17 12 2 m I
public double method3();
descriptor: ()D
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String method3()开始执行...
5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: ldc2_w #14 // double 20.0d
11: dstore_1
12: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #16 // String method3()即将结束...
17: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: dload_1
21: dreturn
LineNumberTable:
line 30: 0
line 31: 8
line 32: 12
line 33: 20
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/atguigu/java1/StackFrameTest;
12 10 1 j D
2.2、堆疊的內部結構
堆疊幀內部結構
每個堆疊幀中存盤著:
- 區域變數表(Local Variables)
- 運算元堆疊(Operand Stack)(或運算式堆疊)
- 動態鏈接(Dynamic Linking)(或指向運行時常量池的方法參考)
- 方法回傳地址(Return Address)(或方法正常退出或者例外退出的定義)
- 一些附加資訊

并行每個執行緒下的堆疊都是私有的,因此每個執行緒都有自己各自的堆疊,并且每個堆疊里面都有很多堆疊幀,堆疊幀的大小主要由區域變數表 和 運算元堆疊決定的

3、區域變數表
3.1、認識區域變數表
認識區域變數表
- 區域變數表: Local Variables,被稱之為區域變數陣列或本地變數表
- 定義為一個 數字陣列,主要用于 存盤方法引數和定義在方法體內的區域變數,這些資料型別包括各類基本資料型別、物件參考(reference),以及returnAddress型別,
- 由于區域變數表是建立在執行緒的堆疊上,是執行緒的私有資料,因此 不存在資料安全問題
- 區域變數表所需的容量大小是在編譯期確定下來的,并保存在方法的Code屬性的 maximum local variables資料項中,在方法運行期間是不會改變區域變數表的大小的,
- 方法嵌套呼叫的次數由堆疊的大小決定,一般來說,堆疊越大,方法嵌套呼叫次數越多,
- 對一個函式而言,它的引數和區域變數越多,使得區域變數表膨脹,它的堆疊幀就越大,以滿足方法呼叫所需傳遞的資訊增大的需求,
- 進而函式呼叫就會占用更多的堆疊空間,導致其嵌套呼叫次數就會減少,
- 區域變數表中的變數只在當前方法呼叫中有效,
- 在方法執行時,虛擬機通過使用區域變數表完成引數值到引數變數串列的傳遞程序,
- 當方法呼叫結束后,隨著方法堆疊幀的銷毀,區域變數表也會隨之銷毀,
區域變數表所需的容量大小是在編譯期確定下來的
- 代碼
public class LocalVariablesTest {
private int count = 0;
public static void main(String[] args) {
LocalVariablesTest test = new LocalVariablesTest();
int num = 10;
test.test1();
}
public void test1() {
Date date = new Date();
String name1 = "atguigu.com";
test2(date, name1);
System.out.println(date + name1);
}
public String test2(Date dateP, String name2) {
dateP = null;
name2 = "songhongkang";
double weight = 130.5;
char gender = '男';
return dateP + name2;
}
}
- 反編譯后,可得結論:
- 在編譯期間,區域變數的個數、每個區域變數的大小都已經被記錄下來
- 所以區域變數表所需的容量大小是在編譯期確定下來的

- 利用 JClassLib 也可以查看區域變數的個數

思考:
- 代碼
public static void main(String[] args) {
if(args == null){
LocalVariablesTest test = new LocalVariablesTest();
}
int num = 10;
}
- 反編譯后,提出問題:上面代碼中的 test 變數跑哪兒哪了呢?
- 我估計 test 變數和 num 變數共用一個 slot

位元組碼中方法內部結構的剖析
- [Ljava/lang/String] :
- [] 表示陣列
- L 表示參考型別
- java/lang/String 表示 java.lang.String
- 合起來就是:main() 方法的形參型別為 String[]

- 位元組碼,位元組碼長度為 16(0~15)

- 方法例外資訊表

- 雜項(Misc)

- 位元組碼指令行號和原始 java 代碼行號的對應關系

- 注意:生效行數和剩余有效行數都是針對于位元組碼檔案的行數

3.2、關于 Slot 的理解
關于 Slot 的理解
- 引數值的存放總是 從區域變數陣列索引 0 的位置開始,到陣列長度-1的索引結束,
- 區域變數表,最基本的存盤單元是Slot(變數槽),區域變數表中存放編譯期可知的各種基本資料型別(8種),參考型別(reference),returnAddress型別的變數,
- 在區域變數表里, 32位以內的型別只占用一個slot(包括returnAddress型別), 64位的型別占用兩個slot(1ong和double),
- JVM會為區域變數表中的每一個Slot都分配一個訪問索引,通過這個索引即可成功訪問到區域變數表中指定的區域變數值
- 當一個實體方法被呼叫的時候,它的方法引數和方法體內部定義的區域變數將會 按照順序被復制到區域變數表中的每一個slot上
- 如果需要訪問區域變數表中一個64bit的區域變數值時,只需要使用前一個索引即可,(比如:訪問long或doub1e型別變數)
- 如果當前幀是由 構造方法或者實體方法創建的,那么 該物件參考this將會存放在index為0的slot處,其余的引數按照引數表順序繼續排列,

Slot 代碼示例
this 存放在 index = 0 的位置:
- 代碼
public void test3() {
this.count++;
}
- 區域變數表:this 存放在 index = 0 的位置

64位的型別(1ong和double)占用兩個slot
- 代碼
public String test2(Date dateP, String name2) {
dateP = null;
name2 = "songhongkang";
double weight = 130.5;
char gender = '男';
return dateP + name2;
}
- weight 為 double 型別,index 直接從 3 蹦到了 5

static 無法呼叫 this
- this 不存在與 static 方法的區域變數表中,所以無法呼叫
public static void testStatic(){
LocalVariablesTest test = new LocalVariablesTest();
Date date = new Date();
int count = 10;
System.out.println(count);
}
3.3、Slot 的重復利用
Slot 的重復利用
堆疊幀中的區域變數表中的槽位是 可以重用的, 如果一個區域變數過了其作用域,那么在其作用域之后申明新的區域變數變就很有可能會復用過期區域變數的槽位,從而達到節省資源的目的,
- 代碼
public void test4() {
int a = 0;
{
int b = 0;
b = a + 1;
}
int c = a + 1;
}
- 區域變數 c 重用了區域變數 b 的 slot 位置

靜態變數與區域變數的對比
變數的分類:
- 按照資料型別分:
- 基本資料型別
- 參考資料型別
- 按照在類中宣告的方式分:
- 類變數:
- linking的prepare階段:給類變數默認賦值
- initial階段:給類變數顯式賦值即靜態代碼塊賦值
-
實體變數:隨著物件的創建,會在堆空間中分配實體變數空間,并進行默認賦值
-
區域變數:在使用前,必須要進行顯式賦值的!否則,編譯不通過,應該是堆疊中資料彈出后,不會清除上次的值,再次使用時,如果不顯示初始化,就會出現臟資料
-
引數表分配完畢之后,再根據方法體內定義的變數的順序和作用域分配,
-
我們知道 類變數表有兩次初始化的機會, 第一次是在"準備階段",執行系統初始化,對類變數設定零值,另一次則是在"初始化"階段,賦予程式員在代碼中定義的初始值,
-
和類變數初始化不同的是, 區域變數表不存在系統初始化的程序,這意味著一旦定義了區域變數則必須人為的初始化,否則無法使用,
代碼示例
- 報錯:區域變數未初始化

補充說明
- 在堆疊幀中,與性能調優關系最為密切的部分就是前面提到的區域變數表,在方法執行時,虛擬機使用區域變數表完成方法的傳遞,
- 區域變數表中的變數也是重要的垃圾回收根節點,只要被區域變數表中直接或間接參考的物件都不會被回收,
4、運算元堆疊
4.1、運算元堆疊的特點
運算元堆疊的特點
運算元堆疊:Operand Stack
- 每一個獨立的堆疊幀除了包含區域變數表以外,還包含一個后進先出(Last - In - First -Out)的 運算元堆疊,也可以稱之為 運算式堆疊(Expression Stack)
- 運算元堆疊,在方法執行程序中, 根據位元組碼指令,往堆疊中寫入資料或提取資料,即入堆疊(push)和 出堆疊(pop)
- 某些位元組碼指令將值壓入運算元堆疊,其余的位元組碼指令將運算元取出堆疊,使用它們后再把結果壓入堆疊,比如:執行復制、交換、求和等操作

代碼舉例
- 左邊為 java 源代碼,右邊為 java 代碼編譯生成的位元組碼指令

4.2、運算元堆疊的作用
運算元堆疊的作用
- 運算元堆疊, 主要用于保存計算程序的中間結果,同時作為計算程序中變數臨時的存盤空間,
- 運算元堆疊就是JVM執行引擎的一個作業區,當一個方法剛開始執行的時候,一個新的堆疊幀也會隨之被創建出來,這時方法的運算元堆疊是空的(這個時候陣列是有長度的,只是運算元堆疊為空)
- 每一個運算元堆疊都會擁有一個明確的堆疊深度用于存盤數值,其所需的 最大深度在編譯期就定義好了,保存在方法的Code屬性中,為 maxstack的值,
- 堆疊中的任何一個元素都是可以任意的Java資料型別
- 32bit的型別占用一個堆疊單位深度
- 64bit的型別占用兩個堆疊單位深度
- 運算元堆疊并非采用訪問索引的方式來進行資料訪問的,而是 只能通過標準的入堆疊和出堆疊操作來完成一次資料訪問
- 如果被呼叫的方法帶有回傳值的話,其 回傳值將會被壓入當前堆疊幀的運算元堆疊中,并更新PC暫存器中下一條需要執行的位元組碼指令,
- 運算元堆疊中元素的資料型別必須與位元組碼指令的序列嚴格匹配,這由編譯器在編譯器期間進行驗證,同時在類加載程序中的類檢驗階段的資料流分析階段要再次驗證,
- 另外, 我們說Java虛擬機的解釋引擎是基于堆疊的執行引擎,其中的堆疊指的就是運算元堆疊,
運算元堆疊的深度
通過反編譯生成的位元組碼指令查看運算元堆疊的深度

5、代碼追蹤
運算元堆疊代碼追蹤
- 代碼
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
- 反編譯得到的位元組碼指令
0 bipush 15
2 istore_1
3 bipush 8
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return
程式執行流程如下
- 首先執行第一條陳述句,PC暫存器指向的是0,也就是指令地址為0,然后使用bipush讓運算元15入運算元堆疊,

- 執行完后,讓PC + 1,指向下一行代碼,下一行代碼就是將運算元堆疊的元素存盤到區域變數表1的位置,我們可以看到區域變數表的已經增加了一個元素
- 解釋為什么區域變數表索引從 1 開始,因為該方法為實體方法,區域變數表索引為 0 的位置存放的是 this

- 然后PC+1,指向的是下一行,讓運算元8也入堆疊,同時執行store操作,存入區域變數表中

- 然后從區域變數表中,依次將資料放在運算元堆疊中,等待執行 add 操作


- 然后將運算元堆疊中的兩個元素執行相加操作,并存盤在區域變數表3的位置


關于 int j = 8; 的說明
- 我們反編譯得到的位元組碼指令如下
- 因為 8 可以存放在 byte 型別中,所以壓入運算元堆疊的型別為 byte ,而不是 int ,所以執行的位元組碼指令為 bipush 8
- 然后執行將數值 8 存放在 int 型別的變數中:istore_2

關于呼叫方法,回傳值入運算元堆疊的說明
- 代碼
public int getSum(){
int m = 10;
int n = 20;
int k = m + n;
return k;
}
public void testGetSum(){
int i = getSum();
int j = 10;
}
- getSum() 方法位元組碼指令:最后帶著個 ireturn

- testGetSum() 方法位元組碼指令:一上來就加載 getSum() 方法的回傳值

++i 與 i++ 的區別
- 代碼
public void add(){
int i1 = 10;
i1++;
int i2 = 10;
++i2;
int i3 = 10;
int i4 = i3++;
int i5 = 10;
int i6 = ++i5;
int i7 = 10;
i7 = i7++;
int i8 = 10;
i8 = ++i8;
int i9 = 10;
int i10 = i9++ + ++i9;
}
- 下面,我根據位元組碼指令,簡單說下 i++ 和 ++i 的區別
0 bipush 10
2 istore_1
3 iinc 1 by 1
6 bipush 10
8 istore_2
9 iinc 2 by 1
12 bipush 10
14 istore_3
15 iload_3
16 iinc 3 by 1
19 istore 4
21 bipush 10
23 istore 5
25 iinc 5 by 1
28 iload 5
30 istore 6
32 bipush 10
34 istore 7
36 iload 7
38 iinc 7 by 1
41 istore 7
43 bipush 10
45 istore 8
47 iinc 8 by 1
50 iload 8
52 istore 8
54 bipush 10
56 istore 9
58 iload 9
60 iinc 9 by 1
63 iinc 9 by 1
66 iload 9
68 iadd
69 istore 10
71 return
i++
- java 源代碼
int i3 = 10;
int i4 = i3++;
- 位元組碼指令:
- bipush 10 :將 10 壓入運算元堆疊
- istore_3 :將運算元堆疊中的 10 保存到變數 i3 中
- iload_3 :將變數 i3 的值(10)加載至運算元堆疊中
- iinc 3 by 1:變數 i3 執行 +1 操作
- istore 4:將運算元堆疊中的值保存至變數 i4 中(10)
12 bipush 10
14 istore_3
15 iload_3
16 iinc 3 by 1
19 istore 4
++i
- java 源代碼
int i5 = 10;
int i6 = ++i5;
- 位元組碼指令
- bipush 10 :將 10 壓入運算元堆疊
- istore 5 :將運算元堆疊中的 10 保存到變數 i5 中
- iinc 5 by 1:變數 i5 執行 +1 操作
- iload 5 :將變數 i5 的值(11)加載至運算元堆疊中
- istore 6:將運算元堆疊中的值保存至變數 i6 中(11)
21 bipush 10
23 istore 5
25 iinc 5 by 1
28 iload 5
30 istore 6
總結:
- i++:先將 i 的值加載到運算元堆疊,再將 i 的值加 1
- ++i:先將 i 的值加 1,在將 i 的值加載到運算元堆疊
6、堆疊頂快取技術
堆疊頂快取技術:Top Of Stack Cashing
- 前面提過,基于堆疊式架構的虛擬機所使用的零地址指令更加緊湊,但完成一項操作的時候必然需要使用更多的入堆疊和出堆疊指令,這同時也就意味著將需要更多的指令分派(instruction dispatch)次數和記憶體讀/寫次數,
- 由于運算元是存盤在記憶體中的,因此頻繁地執行記憶體讀/寫操作必然會影響執行速度,為了解決這個問題,HotSpot JVM的設計者們提出了堆疊頂快取(Tos,Top-of-Stack Cashing)技術,將堆疊頂元素全部快取在物理CPU的暫存器中,以此降低對記憶體的讀/寫次數,提升執行引擎的執行效率,
- 暫存器的主要優點:指令更少,執行速度快
7、動態鏈接
動態鏈接(或指向運行時常量池的方法參考)
動態鏈接:Dynamic Linking

- 每一個堆疊幀內部都包含 一個指向運行時常量池中該堆疊幀所屬方法的參考
- 包含這個參考的目的就是 為了支持當前方法的代碼能夠實作動態鏈接(Dynamic Linking),比如:invokedynamic指令
- 在Java源檔案被編譯到位元組碼檔案中時, 所有的變數和方法參考都作為符號參考(Symbolic Reference)保存在class檔案的常量池里
- 比如:描述一個方法呼叫了另外的其他方法時,就是通過常量池中指向方法的符號參考來表示的,那么 動態鏈接的作用就是為了將這些符號參考轉換為呼叫方法的直接參考

代碼示例
- 代碼
public class DynamicLinkingTest {
int num = 10;
public void methodA(){
System.out.println("methodA()....");
}
public void methodB(){
System.out.println("methodB()....");
methodA();
num++;
}
}
- 在位元組碼指令中,methodB() 方法中通過 invokevirtual #7 指令呼叫了方法 A
- 那么 #7 是個啥呢?
public void methodB();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String methodB()....
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #7 // Method methodA:()V
12: aload_0
13: dup
14: getfield #2 // Field num:I
17: iconst_1
18: iadd
19: putfield #2 // Field num:I
22: return
LineNumberTable:
line 16: 0
line 18: 8
line 20: 12
line 21: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/atguigu/java1/DynamicLinkingTest;
- 往上面翻,找到常量池的定義:
#7 = Methodref #8.#31- 先找 #8 :
#8 = Class #32:去找 #32#32 = Utf8 com/atguigu/java1/DynamicLinkingTest- 結論:通過 #8 我們找到了
DynamicLinkingTest這個類
- 再來找 #31:
#31 = NameAndType #19:#13:去找 #19 和 #13#19 = Utf8 methodA:方法名為 methodA#13 = Utf8 ()V:方法沒有形參,回傳值為 void
- 先找 #8 :
- 結論:通過 #7 我們就能找到需要呼叫的 methodA() 方法,并進行呼叫
Constant pool:
#1 = Methodref #9.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#24 // com/atguigu/java1/DynamicLinkingTest.num:I
#3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #27 // methodA()....
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = String #30 // methodB()....
#7 = Methodref #8.#31 // com/atguigu/java1/DynamicLinkingTest.methodA:()V
#8 = Class #32 // com/atguigu/java1/DynamicLinkingTest
#9 = Class #33 // java/lang/Object
#10 = Utf8 num
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/atguigu/java1/DynamicLinkingTest;
#19 = Utf8 methodA
#20 = Utf8 methodB
#21 = Utf8 SourceFile
#22 = Utf8 DynamicLinkingTest.java
#23 = NameAndType #12:#13 // "<init>":()V
#24 = NameAndType #10:#11 // num:I
#25 = Class #34 // java/lang/System
#26 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#27 = Utf8 methodA()....
#28 = Class #37 // java/io/PrintStream
#29 = NameAndType #38:#39 // println:(Ljava/lang/String;)V
#30 = Utf8 methodB()....
#31 = NameAndType #19:#13 // methodA:()V
#32 = Utf8 com/atguigu/java1/DynamicLinkingTest
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (Ljava/lang/String;)V
</init></init></init>
- 在上面,其實還有很多符號參考,比如 Object、System、PrintStream 等等
為什么要用常量池呢?
- 因為在不同的方法,都可能呼叫常量或者方法,所以 只需要存盤一份即可,然后記錄其參考即可,節省了空間
- 常量池的作用:就是為了提供一些符號和常量,便于指令的識別
8、決議和分派
8.1、靜態鏈接與動態鏈接
靜態鏈接機制與動態鏈接機制
在JVM中,將符號參考轉換為呼叫方法的直接參考與方法的系結機制相關
- 靜態鏈接: 當一個位元組碼檔案被裝載進JVM內部時, 如果被呼叫的目標方法在編譯期確定,且運行期保持不變時,這種情況下將呼叫方法的符號參考轉換為直接參考的程序稱之為靜態鏈接
- 動態鏈接: 如果被呼叫的方法在編譯期無法被確定下來,也就是說, 只能夠在程式運行期將呼叫的方法的符號轉換為直接參考,由于這種參考轉換程序具備 動態性,因此也被稱之為動態鏈接,
8.2、早期系結與晚期系結
方法的系結機制
靜態鏈接和動態鏈接對應的方法的系結機制為:早期系結(Early Binding)和晚期系結(Late Binding), 系結是一個欄位、方法或者類在符號參考被替換為直接參考的程序,這僅僅發生一次,
- 早期系結 早期系結就是指被呼叫的目標方法如果在編譯期可知,且運行期保持不變時,即可將這個方法與所屬的型別進行系結,這樣一來,由于明確了被呼叫的目標方法究竟是哪一個,因此也就 可以使用靜態鏈接的方式將符號參考轉換為直接參考,
- 晚期系結 如果被呼叫的方法在編譯期無法被確定下來, 只能夠在程式運行期根據實際的型別系結相關的方法,這種系結方式也就被稱之為晚期系結,
代碼示例
- 代碼
class Animal {
public void eat() {
System.out.println("動物進食");
}
}
interface Huntable {
void hunt();
}
class Dog extends Animal implements Huntable {
@Override
public void eat() {
System.out.println("狗吃骨頭");
}
@Override
public void hunt() {
System.out.println("捕食耗子,多管閑事");
}
}
class Cat extends Animal implements Huntable {
public Cat() {
super();
}
public Cat(String name) {
this();
}
@Override
public void eat() {
super.eat();
System.out.println("貓吃魚");
}
@Override
public void hunt() {
System.out.println("捕食耗子,天經地義");
}
}
public class AnimalTest {
public void showAnimal(Animal animal) {
animal.eat();
}
public void showHunt(Huntable h) {
h.hunt();
}
}
- invokevirtual 體現為晚期系結

- invokeinterface 也體現為晚期系結

- invokespecial 體現為早期系結

8.3、多型性與方法系結
多型性與方法系結機制
- 隨著高級語言的橫空出世,類似于Java一樣的基于面向物件的編程語言如今越來越多,盡管這類編程語言在語法風格上存在一定的差別,但是它們彼此之間始終保持著一個共性,那就是都支持封裝、繼承和多型等面向物件特性,既然這一類的編程語言具備多型特性,那么自然也就具備早期系結和晚期系結兩種系結方式,
- Java中任何一個普通的方法其實都具備虛函式的特征,它們相當于C++語言中的虛函式(C++中則需要使用關鍵字virtual來顯式定義),如果在Java程式中不希望某個方法擁有虛函式的特征時,則可以使用關鍵字final來標記這個方法,
虛方法與非虛方法
虛方法與非虛方法的區別
- 如果方法在編譯期就確定了具體的呼叫版本,這個版本在運行時是不可變的,這樣的方法稱為非虛方法,
- 靜態方法、私有方法、fina1方法、實體構造器、父類方法都是非虛方法,
- 其他方法稱為虛方法,
子類物件的多型的使用前提:
- 類的繼承關系
- 方法的重寫
虛擬機中呼叫方法的指令
四條普通指令:
- invokestatic:呼叫靜態方法,決議階段確定唯一方法版本
- invokespecial:呼叫
<init></init>方法、私有及父類方法,決議階段確定唯一方法版本 - invokevirtual:呼叫所有虛方法
- invokeinterface:呼叫介面方法
一條動態呼叫指令
invokedynamic:動態決議出需要呼叫的方法,然后執行
區別
- 前四條指令固化在虛擬機內部,方法的呼叫執行不可人為干預
- 而invokedynamic指令則支持由用戶確定方法版本
- 其中invokestatic指令和invokespecial指令呼叫的方法稱為非虛方法,其余的(fina1修飾的除外)稱為虛方法,
代碼示例:
- 代碼
class Father {
public Father() {
System.out.println("father的構造器");
}
public static void showStatic(String str) {
System.out.println("father " + str);
}
public final void showFinal() {
System.out.println("father show final");
}
public void showCommon() {
System.out.println("father 普通方法");
}
}
public class Son extends Father {
public Son() {
super();
}
public Son(int age) {
this();
}
public static void showStatic(String str) {
System.out.println("son " + str);
}
private void showPrivate(String str) {
System.out.println("son private" + str);
}
public void show() {
showStatic("atguigu.com");
super.showStatic("good!");
showPrivate("hello!");
showFinal();
super.showCommon();
showCommon();
info();
MethodInterface in = null;
in.methodA();
}
public void info() {
}
public void display(Father f) {
f.showCommon();
}
public static void main(String[] args) {
Son so = new Son();
so.show();
}
}
interface MethodInterface {
void methodA();
}
- Son 類中 show() 方法的位元組碼指令如下

關于 invokedynamic 指令
- JVM位元組碼指令集一直比較穩定,一直到Java7中才增加了一個invokedynamic指令,這是Java為了實作【動態型別語言】支持而做的一種改進,
- 但是在Java7中并沒有提供直接生成invokedynamic指令的方法,需要借助ASM這種底層位元組碼工具來產生invokedynamic指令,直到Java8的Lambda運算式的出現,invokedynamic指令的生成,在Java中才有了直接的生成方式,
- Java7中增加的動態語言型別支持的本質是對Java虛擬機規范的修改,而不是對Java語言規則的修改,這一塊相對來講比較復雜,增加了虛擬機中的方法呼叫,最直接的受益者就是運行在Java平臺的動態語言的編譯器,
代碼示例
- 代碼
@FunctionalInterface
interface Func {
public boolean func(String str);
}
public class Lambda {
public void lambda(Func func) {
return;
}
public static void main(String[] args) {
Lambda lambda = new Lambda();
Func func = s -> {
return true;
};
lambda.lambda(func);
lambda.lambda(s -> {
return true;
});
}
}
- 位元組碼指令

8.4、方法重寫的本質
動態語言和靜態語言
- 動態型別語言和靜態型別語言兩者的區別就在于 對型別的檢查是在編譯期還是在運行期,滿足前者就是靜態型別語言,反之是動態型別語言,
- 說的再直白一點就是,靜態型別語言是判斷變數自身的型別資訊;動態型別語言是判斷變數值的型別資訊,變數沒有型別資訊,變數值才有型別資訊,這是動態語言的一個重要特征,
Java:String info = "mogu blog"; (Java是静态类型语言的,会先编译就进行类型检查)
JS:var name = "shkstart"; var name = 10; (运行时才进行检查)
方法重寫的本質
Java 語言中方法重寫的本質:
- 找到運算元堆疊頂的第一個元素所執行的物件的 實際型別,記作C,
- 如果 在型別C中找到與常量中的描述符合簡單名稱都相符的方法,則進行訪問權限校驗
- 如果通過則回傳這個方法的直接參考,查找程序結束
- 如果不通過,則回傳java.1ang.IllegalAccessError 例外
- 否則, 按照繼承關系從下往上依次對C的各個父類進行第2步的搜索和驗證程序,
- 如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError例外,
IllegalAccessError介紹
- 程式試圖訪問或修改一個屬性或呼叫一個方法,這個屬性或方法,你沒有權限訪問,
- 一般的,這個會引起編譯器例外,這個錯誤如果發生在運行時,就說明一個類發生了不兼容的改變,
- 比如,你把應該有的jar包放從工程中拿走了,或者Maven中存在jar包沖突
回看決議階段
- 決議階段就是 將常量池內的符號參考轉換為直接參考的程序
- 決議動作主要針對類或介面、欄位、類方法、介面方法、方法型別等,對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
8.5、多型與虛方法表
虛方法表
- 在面向物件的編程中,會很頻繁的使用到 動態分派,如果在每次動態分派的程序中都要重新在類的方法元資料中搜索合適的目標的話就可能 影響到執行效率,
- 因此,為了提高性能, JVM采用在類的方法區建立一個虛方法表(virtual method table)來實作,非虛方法不會出現在表中,使用索引表來代替查找,
- 每個類中都有一個虛方法表,表中存放著各個方法的實際入口,
- 虛方法表是什么時候被創建的呢? 虛方法表會在類加載的鏈接階段被創建并開始初始化,類的變數初始值準備完成之后,JVM會把該類的虛方法表也初始化完畢,
- 如圖所示:如果類中重寫了方法,那么呼叫的時候,就會直接在該類的虛方法表中查找

9、方法回傳地址
方法回傳地址(return address)
- 存放呼叫該方法的pc暫存器的值,一個方法的結束,有兩種方式:
- 正常執行完成
- 出現未處理的例外,非正常退出
- 無論通過哪種方式退出,在方法退出后都回傳到該方法被呼叫的位置,方法正常退出時, 呼叫者的pc計數器的值作為回傳地址,即呼叫該方法的指令的下一條指令的地址,而通過例外退出的, 回傳地址是要通過例外表來確定,堆疊幀中一般不會保存這部分資訊,
- 本質上,方法的退出就是當前堆疊幀出堆疊的程序,此時,需要恢復上層方法的區域變數表、運算元堆疊、將回傳值壓入呼叫者堆疊幀的運算元堆疊、設定PC暫存器值等,讓呼叫者方法繼續執行下去,
- 正常完成出口和例外完成出口的區別在于:通過例外完成出口退出的不會給他的上層呼叫者產生任何的回傳值,
方法退出的兩種方式
當一個方法開始執行后,只有兩種方式可以退出這個方法,
正常退出:
- 執行引擎遇到任意一個方法回傳的位元組碼指令(return),會有回傳值傳遞給上層的方法呼叫者,簡稱正常完成出口;
- 一個方法在正常呼叫完成之后,究竟需要使用哪一個回傳指令,還需要根據方法回傳值的實際資料型別而定,
- 在位元組碼指令中,回傳指令包含:
- ireturn:當回傳值是boolean,byte,char,short和int型別時使用
- lreturn:Long型別
- freturn:Float型別
- dreturn:Double型別
- areturn:參考型別
- return:回傳值型別為void的方法、實體初始化方法、類和介面的初始化方法
例外退出:
- 在方法執行程序中遇到例外(Exception),并且這個例外沒有在方法內進行處理,也就是只要在本方法的例外表中沒有搜索到匹配的例外處理器,就會導致方法退出,簡稱例外完成出口,
- 方法執行程序中,拋出例外時的例外處理,存盤在一個例外處理表,方便在發生例外的時候找到處理例外的代碼

代碼舉例
- 代碼
public class ReturnAddressTest {
public boolean methodBoolean() {
return false;
}
public byte methodByte() {
return 0;
}
public short methodShort() {
return 0;
}
public char methodChar() {
return 'a';
}
public int methodInt() {
return 0;
}
public long methodLong() {
return 0L;
}
public float methodFloat() {
return 0.0f;
}
public double methodDouble() {
return 0.0;
}
public String methodString() {
return null;
}
public Date methodDate() {
return null;
}
public void methodVoid() {
}
static {
int i = 10;
}
public void method2() {
methodVoid();
try {
method1();
} catch (IOException e) {
e.printStackTrace();
}
}
public void method1() throws IOException {
FileReader fis = new FileReader("atguigu.txt");
char[] cBuffer = new char[1024];
int len;
while ((len = fis.read(cBuffer)) != -1) {
String str = new String(cBuffer, 0, len);
System.out.println(str);
}
fis.close();
}
}
- 方法正常回傳
- ireturn

- dreturn

- areturn

- ireturn
- 例外處理表:
- 反編譯位元組碼檔案,可得到 Exception table
- from :位元組碼指令起始地址
- to :位元組碼指令結束地址
- target :出現例外跳轉至地址為 11 的指令執行
- type :捕獲例外的型別

10、一些附加資訊
堆疊幀中還允許攜帶與Java虛擬機實作相關的一些附加資訊,例如:對程式除錯提供支持的資訊,
11、堆疊相關面試題
舉例堆疊溢位的情況?(StackOverflowError)
通過 -Xss 設定堆疊的大小
調整堆疊大小,就能保證不出現溢位么?
不能保證不溢位
分配的堆疊記憶體越大越好么?
不是,一定時間內降低了OOM概率,但是會擠占其它的執行緒空間,因為整個虛擬機的記憶體空間是有限的
垃圾回收是否涉及到虛擬機堆疊?
不會
方法中定義的區域變數是否執行緒安全?
何為執行緒安全?
- 如果只有一個執行緒才可以操作此資料,則必是執行緒安全的,
- 如果有多個執行緒操作此資料,則此資料是共享資料,如果不考慮同步機制的話,會存在執行緒安全問題,
具體問題具體分析:
-
如果物件是在內部產生,并在內部消亡,沒有回傳到外部,那么它就是執行緒安全的,反之則是執行緒不安全的,
-
看代碼
public class StringBuilderTest {
public static void method1(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
public static void method2(StringBuilder sBuilder){
sBuilder.append("a");
sBuilder.append("b");
}
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
new Thread(() -> {
s.append("a");
s.append("b");
}).start();
method2(s);
}
}
運行時資料區,哪些部分存在Error和GC?
運行時資料區是否存在Error是否存在GC程式計數器否否虛擬機堆疊是(SOF)否本地方法堆疊是否方法區是(OOM)是堆是(OOM)是
你只管學習,我來負責記筆記?? 關注公眾號! ,更多筆記,等你來拿,謝謝





轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/164460.html
標籤:其他
