1) 概述
- JVM的字節碼執行引擎,功能基本就是輸入字節碼文件,然后對字節碼進行解析并處理,最后輸出執行的結果。
- 實現方式可能有通過解釋器直接執行字節碼,或者適通過及時編譯器產生本地代碼,也就是編譯執行,當讓也可能兩者皆有。
- HotSpot就是兩者皆有,使用頻率較多的代碼JIT動態編譯為本地代碼,頻率較少的就解釋執行。
2) 棧幀概述
- 棧幀是用于執行JVM進行方法調用和方法執行的數據結構。
- 棧幀隨著方法調用而創建,隨著方法結束而銷毀。
- 棧幀里面存儲了方法的 局部變量、操作數棧、動態連接、方法返回地址等信息。
棧幀的概念結構.png
3) 局部變量表
- 局部變量表:用來存放方法參數和方法內部定義的局部變量的存儲空間。
- 以slot為單位,目前一個slot存放32位以內的數據類型
- 對于64位的數據占2個slot
- 對于實例方法,第0位slot存放的是this,然后從1到n,依次分配給參數列表
- 對于靜態方法,則從0位開始依次分配給參數列表
- 然后根據方法體內部定義的變量順序和作用域來分配slot
public class Test1 {
public int add(int a, int b) {
int c = a + b;
return a + b + c;
/* javap -verbose 得到slot分配情況
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/jvm/stack/Test1;
0 10 1 a I
0 10 2 b I
4 6 3 c I
*/
}
}
- slot是復用的,以節省棧幀的空間,這種設計可能會影響到系統的垃圾收集行為。
// -Xms10m -Xmx10m
public static void main(String[] args) {
{
byte[] bs1 = new byte[1024 * 1204];
byte[] bs2 = new byte[1024 * 1204];
byte[] bs3 = new byte[1024 * 1204];
/* 此時slot的情況
0 -- args -- 堆引用
1 -- bs1 -- 堆引用
2 -- bs2 -- 堆引用
3 -- bs3 -- 堆引用
*/
System.gc();
printMemory(); // freeMemory :2.98663330078125
bs1 = null;
/* 顯式將bs1置為null, slot 1則空閑出來*/
}
System.gc();
printMemory(); // freeMemory :4.8661346435546875
/* 代碼塊結束,上述的bs1 bs2 bs3變量的作用域已經結束,
所占用的slot可以被后續定義的變量復用 */
int a = 5;
int b = 5;
/* 此時slot的使用情況
0 -- args -- 堆引用
1 -- a -- I
2 -- b -- I
3 -- bs3 -- 堆引用
*/
System.gc();
printMemory(); // freeMemory :6.8662872314453125
String c = "a";
/* 此時slot的使用情況
0 -- args -- 堆引用
1 -- a -- I
2 -- b -- I
3 -- c -- 堆引用
*/
System.gc();
printMemory(); // freeMemory :8.866722106933594
}
public static void printMemory() {
//System.out.println("totalMemory:" + Runtime.getRuntime().totalMemory()/1024.0/1024.0);
System.out.println("freeMemory :" + Runtime.getRuntime().freeMemory()/1024.0/1024.0);
//System.out.println("maxMemory :" + Runtime.getRuntime().maxMemory()/1024.0/1024.0);
}
4) 操作數棧
- 操作數棧:用來存放方法運行期間,各個指令操作的數據。
- 操作數棧中元素的數據類型必須和字節碼指令的順序嚴格匹配
- 數據類型和插槽位置的數據類型必須一一對應:比如指令 iconst_1,那么插槽1位置的類型必須是I
- 虛擬機在實現棧幀的時候可能會做一些優化,讓兩個棧幀出現部分重疊區域,以存放公用的數據
package com.lc.sprnigcloud.stack;
/**
* @author hahadasheng
* @since 2020/12/28
*/
public class Test {
public static void main(String[] args) {
test();
}
/* LocalVariableTable:
Start Length Slot Name Signature
2 51 0 a I
16 37 1 b I
35 18 2 c I
*/
public static void test() {
int a = 1;
a = a++;
/*
0: iconst_1 -> 常數1
1: istore_0 -> 將常數1存放在局部變量表slot_0的位置,也就是a
2: iload_0 -> 將slot_0中存放a的值1 放入棧中
3: iinc 0, 1 -> slot_0中a的值自增1,1 + 1 = 2
6: istore_0 -> 將棧中的值1賦值到slot_0的位置,所以此時a的值還是為1
*/
System.out.println(a); // 1
int b = 1;
b = b++ * ++b;
/*
14: iconst_1 -> 常數1
15: istore_1 -> 將1放在slot_1的位置
16: iload_1 -> 將slot_1的值1入棧 棧:[1]
17: iinc 1, 1 -> slot_1位置的1自增1,1+1=2
20: iinc 1, 1 -> slot_1位置的2自增1,2+1=3
23: iload_1 -> 將slot_1的值3入棧 棧:[3,1]
24: imul -> 將棧中的兩個數相乘 3 * 1 = 3,棧中的值為3
25: istore_1 -> 將棧中值3放在slot_1的位置
*/
System.out.println(b); // 3
int c = 1;
c = ++c * c++;
/*
33: iconst_1 -> 常數1
34: istore_2 -> 將常數1放在局部變量表slot_2的位置
35: iinc 2, 1 -> slot_2位置數自增1,1+1=2
38: iload_2 -> 將slot_2位置的2入棧 棧:[2]
39: iload_2 -> 將slot_2位置的2入棧 棧:[2,2]
40: iinc 2, 1 -> 將slot_2位置的2自增1,2+1=3
43: imul -> 將棧中的值取出來進行乘法操作 2*2=4,棧中的值變為4
44: istore_2 -> 將棧中值4存放在slot_2的位置
*/
System.out.println(c); // 4
}
}
5) 動態連接
- 動態連接:每個棧幀持有一個指向運行時常量池中該棧幀所屬方法的引用,以支持方法調用過程中的動態連接。
- 動態連接分類:
- 靜態解析:類加載的時候,符號引用就轉化成直接引用
- 動態連接:運行期間轉換為直接引用(動態分派)
6) 方法返回地址
- 方法返回地址:方法執行后返回的地址,
- 無論正常退出還是異常退出,都得返回到方法被調用的位置,程序才能繼續執行
7) 方法調用
- 方法調用:方法調用就是確定具體調用哪一個方法,并不涉及方法內部的執行過程。
- 不一定要調用這個方法
- 部分方法是直接在類加載的解析階段,就確定了直接引用關系
- 靜態方法、私有方法、實例構造器、父類方法
- 但是對于實例方法,也稱虛方法,因為重載和多態,需要運行期間動態委派
- 例如虛擬機調用此方法的指令為
invokevirtual
- 例如虛擬機調用此方法的指令為
8) 靜態分派和動態分派
分派:分為靜態分派和動態分派
-
靜態分派:所有依賴靜態類型來定位方法執行版本的分派方式
- 比如:重載方法(依據傳參確定)
-
動態分派:根據運行期間的實際類型來定位方法執行版本的分派方式,
- 比如:重寫覆蓋方法、實現的接口等
-
單分派和多分派:就是按照分派思考的緯度,多于一個的就算多分派,只有一個的稱為單分派
- 只有一個確認的,沒有重載、重寫、多態等可能有多個可能的就是單分派
如何執行方法中的字節碼指令:JVM通過基于棧的字節碼解釋器引擎來執行指令,JVM的指令集也是基于棧的。