JVM字節碼執行引擎思惟導圖

JVM字節碼執行引擎

本文參考自來自周志明《深刻理解Java虛擬機(第2版)》,拓展內容建議讀者能夠閱讀下這本書。

文字版以下:java

運行時棧幀結構

局部變量表

  • 須要多少大小的局部變量表已寫入到class字節碼方法的Code屬性的max_locals屬性中
  • 一個存儲單位稱爲一個Slot(32位)緩存

    • 爲了讓全部數據類型的局部變量都可以存儲到局部變量表中而設定了定長的Slot長度ide

      • 32位之內的數據類型(boolean | byte | char | short | int | float | reference | returnAddress)變量使用1個Slot存儲
      • 32位以上的數據類型(long | double)變量使用2個Slot存儲,會以高位對齊的方式進行存儲
  • 局部變量第0位Slot優化

    • 實例方法的局部變量表的第0位存儲的是用於傳遞方法所屬對象實例的this引用
    • 靜態方法的局部變量表沒有這個this引用
  • Slot重用:離開了做用域的局部變量佔用的Slot能夠被重用this

    • 對垃圾回收的影響:離開了做用域的局部變量所在的Slot只有在真的不存在對原有局部變量的引用時,GC纔會回收相應的對象實例spa

      • 花括號內的局部變量a在花括號外出了做用域,直接GC也不會回收a,就是因爲這時仍是存在Slot對a的引用
      • 花括號內的局部變量a在花括號外出了做用域,花括號外再有一個局部變量賦值致使Slot被重用了,GC就會回收a,就是因爲Slot對a的引用沒有了
  • 局部變量和類字段的區別code

    • 生命週期不一樣對象

      • 類字段的生命週期是類的生命週期
      • 局部變量的生命週期是方法中變量的生命週期
    • 賦初值不一樣繼承

      • 類字段會在類加載的準備階段賦零值,再會在類加載的初始化賦初值
      • 局部變量的初值若是沒有手動賦值的話是沒有的,編譯階段即會報出未初始化就使用的錯誤
  • reference類型索引

    • 能夠從reference類型變量中直接或間接地查找到對象在Java堆中存放的起始地址索引
    • 能夠從reference類型變量中直接或間接地查找到對象所屬類型在方法區中的類的存儲信息

操做數棧

  • 須要多大的棧深度已寫入到class字節碼方法的Code屬性的max_stacks屬性中,任什麼時候候棧容量都不能超過最大棧深度
  • 一個存儲單位稱爲一個棧容量

    • 爲了讓全部數據類型的局部變量都可以存儲到棧中而設定了定長的棧容量

      • 32位之內的數據類型(boolean | byte | char | short | int | float | reference | returnAddress)變量使用棧容量爲1
      • 32位以上的數據類型(long | double)變量使用棧容量爲2

動態連接

  • 爲了知足方法指令的運行時解析

    • 靜態解析:把方法調用指令中的符號引用在類加載或首次運行時轉換爲直接引用,即這些方法引用是靜態可肯定的引用
    • 動態連接:把方法調用指令中的符號引用在運行時才轉換爲直接引用,即這些方法引用是動態運行時纔可肯定的

方法返回地址

  • 退出方法的方式

    • 正常返回出口:執行到返回指令字節碼的時候會將方法返回值傳遞給上層的方法調用者
    • 異常完成出口:方法執行遭遇異常,如JVM內部異常或者athrow字節碼指令,且在異常表中未找到匹配處理器致使退出,不會給上層的方法調用者任何返回值
  • 退出方法執行的操做

    • 恢復上層方法的局部變量表
    • 把返回值壓入調用者棧幀的操做數棧
    • 調整PC計數器的值指向方法調用指令的後一條指令

附加信息

方法調用

方法執行的步驟

  • 方法編譯:將源碼編譯稱class字節碼中的符號引用
  • 方法調用:將class字節碼中的符號引用解析成直接引用
  • 方法執行:執行解析出的直接引用指向地址的方法

虛方法和非虛方法

  • 非虛方法:在類加載的時候就能夠將符號引用解析爲直接引用的方法,即不存在因覆蓋而產生多版本的方法

    • 靜態方法
    • 實例構造器<init>方法
    • 私有方法
    • 父類方法
    • final方法
  • 虛方法:在類加載的時候有可能沒法肯定符號引用能夠解析爲哪一個直接引用的方法

    • 因覆蓋父類方法而出現的多版本方法沒法在類加載時就肯定方法的直接引用
    • 目前沒有產生覆蓋的方法雖然不存在多版本可是也歸爲了虛方法,由於它可能會被新類覆蓋方法

5種方法調用字節碼

  • invokestatic

    • 靜態方法
  • invokespecial

    • 實例構造器<init>方法
    • 私有方法
    • 父類方法
  • invokevirtual

    • final方法
    • 虛方法
  • invokeinterface

    • 接口方法,會在運行時再肯定實現接口的對象
  • invokedynamic

    • 運行時動態解析出調用點限定符所引用的方法再執行

分派 Dispatch

  • 靜態分派 Method Overload Resolution

    • 編譯期便可肯定方法的版本
    • 由編譯器直接解析來完成方法版本的斷定,不須要虛擬機介入
    • 典型應用是方法重載 Overload

      • 重載:方法簽名不一樣(方法名相同、參數列表不一樣)
      • 編譯階段就會將重載方法調用解析爲 invokevirtual 選擇的方法版本的符號引用
      • 方法m(Q)重載了方法m(P),編譯器解析名爲m的方法時會根據其參數p的靜態類型(即外觀類型或者直接類型)來選擇方法的版本
      • 若是參數是無顯式的靜態類型的字面量,那麼編譯期會最大可能地去推斷字面量最貼近的參數類型。如print(‘a’)解析爲重載的方法版本print(char) print(int)時會優先推斷字面量類型爲char選擇前者,若是沒有前者定義的話纔會選擇後者
  • 動態分派

    • 運行期纔可肯定方法的版本
    • 由虛擬機來完成方法版本的斷定
    • 典型應用是方法重寫 Override

      • 重寫:方法簽名相同(方法名相同、參數列表相同)
      • 編譯階段就會將重寫方法調用解析爲 invokevirtual 選擇的方法版本的符號引用
      • 類B的方法m(P)重寫了B所繼承的類A的方法m(P),實際類型爲B,A b = new B(),即靜態類型爲A的對象b,發生了方法調用b.m(p)

        • 編譯期:編譯器將b.m(p)編譯成了invokevirtual A.m
        • 運行期

          • 找到操做數棧頂的第一個元素指向對象即b的實際類型,也就是B
          • 在B中尋找與invokevirtual調用的方法符號引用名稱和簽名一致的方法

            • 找到了的話說明實際類型B中就有了這個方法m(P),而後斷定該方法是否知足訪問權限

              • 知足:將調用的方法符號引用替換爲該方法的直接引用
              • 不知足:拋出java.lang.illegalAccessError異常
            • 沒找到

              • 在B的父類中自下向上尋找與invokevirtual調用的方法符號引用名稱和簽名一致的方法

                • 找到了的話說明實際類型B的父類中有這個方法m(P),而後斷定該方法是否知足訪問權限

                  • 知足:將調用的方法符號引用替換爲該方法的直接引用
                  • 不知足:拋出java.lang.illegalAccessError異常
                • 始終沒找到:拋出java.lang.AbstractMethodError異常

方法宗量:方法的接收者(全部者)和參數

  • 單分派:根據單個方法宗量進行方法選擇

    • 靜態階段(編譯期)的選擇過程是依據方法的接收者和方法的參數兩個總量來選擇的,所以靜態階段多分派
  • 多分派:根據多個方法宗量進行方法選擇

    • 動態階段(運行時)的選擇過程是依據方法的接收者來選擇的,所以動態階段單分派

虛擬機動態分派的實現

  • 搜索虛方法的合適版本:代價太大
  • 穩定優化手段:虛方法表
提早記錄避免運行時搜索的手段

    • 父類類加載時準備虛方法表,虛方法表將每一個虛方法的表中索引指向方法區中的類信息的方法實際地址
    • 子類類加載時準備虛方法表,虛方法表將重寫的方法指向子類在方法區中類信息的方法實際地址,而未重寫的方法仍舊指向其父類方法區中的類信息的方法實際地址
    • 調用虛方法時能夠直接獲取到這個方法的實際地址而免去了搜索過程
  • 非穩定的激進優化手段

    • 內聯緩存
    • 基於「類型繼承關係分析」技術的守護內聯

基於棧的解釋器的執行過程

下面來看一下靜態分派和動態分派的例子

靜態分派,使用方法重載做爲例子看一下編譯期的結果:

源碼定義了繼承了HumanManWoman,關鍵在於看一下編譯器將test.sayHi(human);test.sayHi(woman);test.sayHi(man);編譯成了什麼,也就是說靜態階段編譯器重載方法的方法分派是什麼樣的。

class Human{

}

class Man extends Human{

}

class Woman extends Human{

}

public class DispatchTest {

    public void sayHi(Human human){
        System.out.println("Hi human");
    }

    public void sayHi(Man man){
        System.out.println("Hi man");
    }

    public void sayHi(Woman woman){
        System.out.println("Hi woman");
    }

    public static void main(String[] args) {
        Human human = new Human();
        Human man = new Man();
        Human woman = new Woman();
        DispatchTest test = new DispatchTest();
        test.sayHi(human);
        test.sayHi(woman);
        test.sayHi(man);
    }
}

javap獲取到的class字節碼解釋:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: new           #7                  // class Human
         3: dup
         4: invokespecial #8                  // Method Human."<init>":()V
         7: astore_1
         8: new           #9                  // class Man
        11: dup
        12: invokespecial #10                 // Method Man."<init>":()V
        15: astore_2
        16: new           #11                 // class Woman
        19: dup
        20: invokespecial #12                 // Method Woman."<init>":()V
        23: astore_3
        24: new           #13                 // class DispatchTest
        27: dup
        28: invokespecial #14                 // Method "<init>":()V
        31: astore        4
        33: aload         4
        35: aload_1
        36: invokevirtual #15                 // Method sayHi:(LHuman;)V
        39: aload         4
        41: aload_3
        42: invokevirtual #15                 // Method sayHi:(LHuman;)V
        45: aload         4
        47: aload_2
        48: invokevirtual #15                 // Method sayHi:(LHuman;)V
        51: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      52     0  args   [Ljava/lang/String;
            8      44     1 human   LHuman;
           16      36     2   man   LHuman;
           24      28     3 woman   LHuman;
           33      19     4  test   LDispatchTest;

這段字節碼的表義很清晰,就是建立了類型爲HumanManWoman的三個類的對象實例,而後在執行test.sayHi(human);test.sayHi(woman);test.sayHi(man);的時候都將其編譯成了調用方法sayHi:(LHuman;)V,即參數類型是HumansayHi方法。從這裏咱們能夠看出來實際上靜態編譯重載方法的時候的只會使用方法參數的靜態類型指定的方法,而實際類型因爲在運行時可能會發生變化,沒有辦法在編譯期得到其實際類型,所以採用了這種方式直接肯定方法的分派結果。

動態分派,使用方法重寫做爲例子看一下編譯期的結果:

源碼定義了繼承了HumanManWoman,關鍵在於看一下編譯器將man.sayHi();woman.sayHi();編譯成了什麼,也就是說靜態階段編譯器對重寫方法的方法分派是什麼樣的。

abstract class Human{
    protected abstract void sayHi();
}

class Man extends Human{

    @Override
    protected void sayHi() {
        System.out.println("Hi man");
    }
}

class Woman extends Human{

    @Override
    protected void sayHi() {
        System.out.println("Hi woman");
    }
}

public class DispatchTest {

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        DispatchTest test = new DispatchTest();
        man.sayHi();
        woman.sayHi();
    }
}

javap獲取到的class字節碼解釋:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class Man
         3: dup
         4: invokespecial #3                  // Method Man."<init>":()V
         7: astore_1
         8: new           #4                  // class Woman
        11: dup
        12: invokespecial #5                  // Method Woman."<init>":()V
        15: astore_2
        16: new           #6                  // class DispatchTest
        19: dup
        20: invokespecial #7                  // Method "<init>":()V
        23: astore_3
        24: aload_1
        25: invokevirtual #8                  // Method Human.sayHi:()V
        28: aload_2
        29: invokevirtual #8                  // Method Human.sayHi:()V
        32: return
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            8      25     1   man   LHuman;
           16      17     2 woman   LHuman;
           24       9     3  test   LDispatchTest;

這段字節碼建立了類型爲ManWoman的兩個類的對象實例,而後在執行man.sayHi();woman.sayHi();的時候都將其編譯成了調用方法Human.sayHi:()V,即Human類型的sayHi方法。從這裏咱們能夠看出來實際上靜態編譯重寫方法的時候的只會使用實例對象的靜態類型的方法,而實際類型因爲在運行時可能會發生變化,沒有辦法在編譯期得到其實際類型,所以採用了這種方式直接肯定方法的分派結果。可是在運行時會去獲取這個實例對象的實際類型,而後看這個實際類型是否認了名稱和限定符知足的方法,若是有就會選擇分派到這個方法上。這就是運行時動態分派產生的影響,這種選擇在編譯器是看不出來的。

相關文章
相關標籤/搜索