Java虛擬機執行引擎

執行引擎

關於執行引擎相關的部分, 在以前的博文裏
Java內存區域中已經有所說起.javascript

回顧一下:
也只有幾個概念, JVM方法調用和執行的基礎數據結構是 棧幀, 是內存區域中 虛擬機棧中的棧元素, 每個方法的執行就對應着一個棧幀在虛擬機棧中出棧入棧的過程.html

棧幀:則是包含有局部變量表, 操做數棧, 動態鏈接, 方法返回地址, 附加信息.java

  1. 局部變量表:數據結構

    存儲單位是 slot, 一個slot佔據32位, 對於64位的數據類型, 則是分配連續兩個slot空間. 而對於一個非靜態方法而言, 有一個隱藏參數, 爲 this, 而在局部變量表中的變量存儲順序則是jvm

    this -> 方法參數 -> 方法體內的變量(slot能夠重用, 超出做用域便可複用.) 方法在編譯完成後, 其所需的空間已經肯定.ide

    (這裏也是須要注意的一個地方, 變量的做用域經常會覆蓋整個方法, 即便變量已經再也不使用, 但只要還在做用域內, 其slot空間就沒法給其餘變量使用, 所以, 最好是在須要使用到變量時, 定義在合理的做用域範圍內.)this

  2. 操做數棧:spa

    在操做數棧中須要注意,其數據類型必須與字節碼指令的序列嚴格匹配.code

  3. 動態鏈接: 稍後詳解htm

  4. 方法返回地址:

    方法有兩種退出方式, 正常退出, 異常退出, 當正常退出後, 會恢復上層方法的局部變量表, 操做數棧, 並把方法返回結果壓入調用者的操做數棧.

方法調用

方法調用階段的惟一目的是, 肯定調用方法的版本到底是哪個.

在Java虛擬機中提供了5條方法調用的相關指令:

invokestatic: 調用靜態方法

invokespecial: 調用實例構造器方法, 私有方法, 父類方法

invokevirtual: 調用全部的虛方法

invokeinterface: 調用全部的接口方法

invokedynamic: 先在運行時動態解析出調用點限定符所引用的方法, 而後再執行該方法.

虛方法是非虛方法的補集, 什麼是非虛方法呢? 可以在編譯器就肯定將要調用的到底是哪一個方法, 進而將該方法的符號引用 轉換爲 相應的直接引用的 方法就被稱做非虛方法.

咱們知道在類加載時, 在相應的類信息中, 存有對應方法的相關信息, 常量池中存有相關直接引用. 在類加載的解析階段, 即會將這部分的符號引用轉換爲直接引用.

那麼什麼方法才知足這種條件呢?

可以被invokespecial 和 invokestatic指令調用的方法, 都是能夠在編譯器肯定的方法, 即靜態方法, 私有方法, 父類方法(super.), 實例構造器.

在final方法是個特殊點, 雖然final方法的執行爲 invokevirtual, 但它依然屬於非虛方法, 不難理解, final方法不可以被重寫.

方法分派(dispatch)

  1. 靜態分派

    對於代碼

    Human man = new Man();

    其中Human被稱爲變量的靜態類型, 也叫外觀類型, 而 Man則是變量的實際類型. 而一個變量的靜態類型, 在聲明時即已經肯定, 僅僅在使用時纔可以臨時轉換靜態類型, 但變量自己的靜態類型並不會改變, 實際類型的變化只有在運行期才能肯定.

    //實際類型變化
     Human man = new Man();
     man = new Woman();
     //靜態類型的變化
     method((Man) man);
     method((Woman) man);

    而當咱們在重載方法時, 向方法中傳入的參數類型, 便是靜態類型.所以 重載是一種 能夠在編譯期就被肯定執行方法版本 的行爲.

  2. 動態分派

    動態分派 與 重寫息息相關.

    static class Human{
         void sayHello() {
             System.out.println("human say hello");
         }
     }
    
     static class Man extends Human{
         @Override
         void sayHello() {
             System.out.println("man say hello");
         }
     }
    
     void sayHello(Human man) {
         man.sayHello();
     }
    
     public static void main(String[] args) {
         Human man = new Man();
         Human human = new Human();
         new Main().sayHello(man);
         new Main().sayHello(human);
     }
    
     //out:
     man say hello
     human say hello

    結果沒必要多作解釋, 而如今的問題在於, 虛擬機如何知道, 究竟調用的是哪一個方法?

    0: new           #3                  // class Main$Man
      3: dup
      4: invokespecial #4                  // Method Main$Man."<init>":()V
      7: astore_1
      8: new           #5                  // class Main$Human
     11: dup
     12: invokespecial #6                  // Method Main$Human."<init>":()V
     15: astore_2
     16: new           #7                  // class Main
     19: dup
     20: invokespecial #8                  // Method "<init>":()V
     23: aload_1
     24: invokevirtual #9                  // Method sayHello:(LMain$Human;)V
     27: new           #7                  // class Main
     30: dup
     31: invokespecial #8                  // Method "<init>":()V
     34: aload_2
     35: invokevirtual #9                  // Method sayHello:(LMain$Human;)V
     38: return

    其中主要關注幾個方法的執行點, invokespecial不用多說, 以前提到過, 是執行 構造器方法時 的指令

    而 invokevirtual 則正是執行 main.sayHello(), 方法的指令, 指令的運行時解析過程大體以下:

    1. 找到操做數棧頂的第一個元素的所指對象的實際類型, 記作C

    2. 若是在C中找到與描述符 和 簡單名稱都相符的方法, 進行訪問校驗, 若是能夠則返回方法的直接引用, 不然拋出 IllegalAccessError異常

    3. 不然按照繼承關係 從下向上對C的各個父類進行第二步的搜索驗證過程.

    4. 若是始終找不到, 拋出異常.

    而其中的關鍵點就在於, 取到的是 對象的實際類型.

動態類型語言

這也是要提到的關於 invokedynamic指令的主要目的。

動態類型語言的概念是: 意思就是類型的檢查是在運行時作的而非編譯期。

而Java自己則是靜態類型語言, 這一點又在哪裏可以體現呢?

obj.println("language");

若是處在java環境中,且obj的靜態語言類型是 java.io.PrintStream, 那麼obj自己的實際類型也必須是PrintStream的子類才行, 哪怕自己存在 println方法也不能夠, 但一樣的問題放在 javascript中就不一樣了, 只要實際類型中存在println方法, 執行就不會有任何問題.

這點就是由於, java在編譯時已經將其完整的符號引用生成出來, 若是注意到的話, 會發現不管是動態分派仍是靜態分派, 在編譯的指令中都是已經精確到相應類的某一個方法中了, 如此, 天然只可以在有限的範圍內略作調整, 若是超出了當前類的範圍, 就沒法調用了.

jvm虛擬機並不只僅是java語言的虛擬機, 那麼如何爲動態類型語言提供支持就是一個問題了, 而且在目前java8中的lamda表達式中也應用的是 invokedynamic指令.

MethodHandle

而與之相關的jar包則是 java.lang.invoke, 相關的類則是 MethodHandle.

在這裏我也並不想再談 MethodHandle的使用方法, 網上資料實在很多.

須要提到的是, 它的功能和java的反射略有類似, 經過方法名, class, 就能夠調用相應的方法. 但它比起反射要輕量級; 且Reflection是在模擬Java代碼的調用, MethodHandle是在模仿字節碼層面的調用.

這個方法不失爲是在動態調用中除了反射以外的另外一種選擇.

基於棧解釋器的執行過程

其實本文更像是在 前一篇博客中 java內存區域中的虛擬機棧的一種補充說明.

而真實的執行流程, 我想經過下文的代碼來看:

public int add() {
   int a = 100;
   int b = 200;
   int c = 300;
   return (a + b) * c;
}

-- javap -verbose Main

public int add();
// 返回類型爲 int
descriptor: ()I
flags: ACC_PUBLIC
Code:
//須要深度爲2的操做數棧, 4個slot的局部變量空間, 有一個參數爲 this
  stack=2, locals=4, args_size=1
  //將100推入操做數棧頂, 棧:100
     0: bipush        100
    //將棧頂的數據出棧並存儲到局部變量表的第一個slot中(從0開始)
    //此時:棧: - 局部變量表: slot1 100
     2: istore_1
     //與上面相似,重複過程
     3: sipush        200
     6: istore_2
     7: sipush        300
     //此時:棧: - 局部變量表: slot1 100 slot2 200 slot3 300
    10: istore_3
    //將局部變量表 slot1的值複製到 棧頂
    11: iload_1
    //將局部變量表 slot2的值複製到 棧頂 此時:棧: 200 100
    12: iload_2
    //棧頂兩個元素出棧, 並相加, 結果從新入棧. 此時: 棧: 300
    13: iadd
    //將局部變量表 slot3的值複製到 棧頂 此時:棧: 300 300
    14: iload_3
    //將棧頂元素相乘, 結果從新入棧
    15: imul
    //將棧頂的結果返回給方法調用者. 方法執行結束
    16: ireturn
  LineNumberTable:
    line 85: 0
    line 86: 3
    line 87: 7
    line 88: 11
    //局部變量表, 無需多言.
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      17     0  this   LMain;
        3      14     1     a   I
        7      10     2     b   I
       11       6     3     c   I

基於棧的執行引擎正是經過這樣出棧入棧的方式完成指令, 而基於寄存器的則否則, 是將操做數存入寄存器, 同時將輸入值也就是指令參數 與 某寄存器的存儲值相加. 區別就在於存儲位置, 以及參數問題, 基於棧的大部分指令都是無參數指令, 指令很明確的規定了 要用哪幾個棧元素, 棧元素的類型是什麼.

咱們日常所使用的電腦, 其 X86指令集, 正是基於寄存器的指令集.

優缺點則是: 基於棧, 可移植性較強, 但速度比較慢, 慢的緣由一是須要許多冗餘操做, 代碼. 二是基於棧是基於內存的操做方式, 而內存的速度比起寄存器更是要慢上許多.

至於寄存器爲何比內存更快?

爲何寄存器比內存快?

總結

本文大體介紹這樣幾點:

java多態在 jvm層次的實現.

爲何說jvm執行引擎是基於棧的執行引擎, 以及到底是怎樣一個流程.

相關文章
相關標籤/搜索