虛擬機字節碼執行引擎

1.概述 

  在 Java 虛擬機規範中制定了虛擬機字節碼執行引擎的概念模型。在不一樣的虛擬機實現裏面,執行引擎在執行 Java 代碼的時候可能會有解釋執行(經過解釋器執行)和編譯執行(經過即時編譯器產生本地代碼執行)兩種選擇,也可能二者兼備,甚至還可能會包含幾個不一樣級別的編譯器執行引擎。但從外觀上看起來,全部的 Java 虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果。本章主要從概念模型的角度來說解虛擬機的方法調用和字節碼執行java

2.運行時棧幀結構

  java虛擬機以方法做爲最基本的執行單元。「棧幀」則是用於支持虛擬機進行方法調用和方法執行的數據結構。在編譯java程序源碼的時候,棧幀中須要多大的局部變量表,須要多深的操做數棧已經被分析計算出來,不會受到程序運行期變量數據的影響。安全

  一個線程中方法的調用鏈可能很長,以java角度來看,同一時刻,同一條線程裏面,在調用堆棧的全部方法都同時處於執行狀態。而對於執行引擎來說,在活動線程中,只有位於棧頂的方法纔是運行的,只有位於棧頂的棧幀纔是生效的,其被稱爲當前棧幀。執行引擎所運行的全部字節碼指令都只針對當前棧幀進行操做。數據結構

  2.1局部變量表

  局部變量表是一組變量值的存儲空間,用於存放方法參數和方法內部定義的局部變量。ide

  局部變量表的容量以變量槽爲最小單位,一個變量槽能夠存放一個32位之內的數據類型(boolean,byte,char,short,int,float,reference,returnAddress),其中reference表示對一個對象實例的引用。對於long和double,java虛擬機則會以高位對齊的方式爲其分配兩個連續的變量槽空間。不過因爲局部變量表創建在線程的堆棧中,屬於線程私有的數據,因此不管讀寫兩個連續的變量槽是否爲原子操做,都不會引發線程安全問題。變量槽的長度能夠隨着處理器,操做系統或虛擬機實現的不一樣而發生變化,如64位虛擬機中使用了64位的物理內存空間去實現一個變量槽。spa

  若是遇到一個方法,其後面定義的代碼有一些耗時很長的動做,而前面又定義了佔用大量內存可是實際已不會再使用的變量,能夠手動設置爲null,把變量對應的局部變量槽清空,促進垃圾回收。操作系統

  局部變量不像類變量存在準備階段,不會賦予系統初始值。因此若是一個局部變量定義了可是沒有賦初始值,就徹底不能使用線程

  2.2操做數棧

  當一個方法開始執行的時候,這個方法的操做數棧是空的。在方法的執行過程當中,會有各類字節碼指令往操做數棧寫入和提取內容,也就是出棧和入棧。舉個例子,整數加法的字節碼指令iadd,這條指令在運行時要求操做數棧中最接近棧頂的兩個元素已經存入兩個int類型的數值,當執行這個指令時,會把這兩個int值出棧並想加,而後將相加結果從新入棧。code

  2.3動態連接

  每一個棧幀都包含一個指向運行時常量池中該棧幀所屬性方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接。
  在 Class 文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化爲直接引用,這種轉化稱爲靜態解析。另一部分將在每一次的運行期期間轉化爲直接引用,這部分稱爲動態鏈接。orm

  2.4方法返回地址  

  當一個方法被執行後,有兩種方式退出這個方法:對象

  • 執行引擎遇到任意一個方法返回的字節碼指令,這種退出方法的方式稱爲正常完成出口(Normal Method Invocation Completion)。
  • 方法執行過程當中遇到了異常,而且這個異常沒有在方法體內獲得處理(即本方法異常處理表中沒有匹配的異常處理器),就會致使方法退出,這種退出方式稱爲異常完成出口(Abrupt Method Invocation Completion)。注意:這種退出方式不會給上層調用者產生任何返回值。

  不管採用何種退出方式,在方法退出後,都須要返回到方法被調用的位置,程序才能繼續執行,方法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。通常來講,方法正常退出時,調用者的PC計數器的值能夠做爲返回地址,棧幀中極可能會保存這個計數器值。而方法異常退出時,返回地址是經過異常處理器表來肯定的,棧幀中通常不會保存這部分信息。
  方法退出的過程實際上等同於把當前棧幀出棧,所以退出時可能執行的操做有:恢復上層方法的局部變量表和操做數棧,把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調整PC計數器的值以指向方法調用指令後面的一條指令等。

3.方法調用

  方法調用不等於方法中的代碼執行,惟一的任務就是肯定被調用方法的版本,即調用哪個方法。

  3.1解析

  全部方法調用的目標方法在class文件裏面都是一個常量池中的符號引用,在類加載階段,會將其中的一部分符號引用轉換爲直接引用,這些方法要求運行期是不可改變的。

  在java語言中符合「編譯期可知,運行期不可變」這個要求的方法,主要有靜態方法和私有方法兩大類。這兩種方法各自的特色決定了他們不可能經過繼承或別的方式重寫出其餘版本,所以適合在類加載階段進行解析。另外被final修飾的實例方法,也能夠在類加載階段進行解析

  3.2分派 

  分派調用過程將會揭示多態性特徵的一些最基本的體現,如「重載」和「重寫」在Java虛擬中是如何實現的。

  3.2.1 靜態分派

  全部依賴靜態類型來定位方法執行版本的分派動做,都稱爲靜態分派。靜態分派發生在編譯階段。靜態分派最典型的應用就是方法重載。

package com.ryj.hotspot.dispatch;

public class StaticDispatch {
    static abstract class Human {
    }

    static class Man extends Human {
    }

    static class Woman extends Human {
    }

    public void sayhello(Human guy) {
        System.out.println("Human guy");
    }

    public void sayhello(Man guy) {
        System.out.println("Man guy");

    }

    public void sayhello(Woman guy) {
        System.out.println("Woman guy");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch staticDispatch = new StaticDispatch();
        staticDispatch.sayhello(man);// Human guy
        staticDispatch.sayhello(woman);// Human guy
    }
}

/* 
   result:
    Human guy
    Human guy
*/

  Human man = new Man();其中的Human稱爲變量的靜態類型(Static Type),Man稱爲變量的實際類型(Actual Type)。
  二者的區別是:靜態類型在編譯器可知,而實際類型到運行期才肯定下來。
  在重載時經過參數的靜態類型而不是實際類型做爲斷定依據,所以,在編譯階段,Javac編譯器會根據參數的靜態類型決定使用哪一個重載版本。因此選擇了sayhello(Human)做爲調用目標,並把這個方法的符號引用寫到main()方法裏的兩條invokevirtual指令的參數中。 

  3.2.2 動態分派

  在運行期根據實際類型肯定方法執行版本的分派過程稱爲動態分派。最典型的應用就是方法重寫。

package com.ryj.hotspot.dispatch;

public class DynamicDisptch {
    static abstract class Human {
        abstract void sayhello();
    }

    static class Man extends Human {
        @Override
        void sayhello() {
            System.out.println("man");
        }
    }

    static class Woman extends Human {
        @Override
        void sayhello() {
            System.out.println("woman");
        }
    }

    public static void main(String[] args) {
        Human man = new Man(); 
        man.sayhello();
     Human woman = new Woman(); woman.sayhello(); man
= new Woman(); man.sayhello(); } } /* * result: * man woman woman * */

  這裏選擇調用的方法版本再也不依據靜態類型來作決定,由於靜態類型一樣都是human的兩個變量man和woman在調用sayHello方法時產生了不一樣的行爲。

  那麼java虛擬機是如何根據實際類型來分派方法的執行版本呢?

  

  根據《java虛擬機規範》,invokevirtual指令的運行時解析過程大體分爲如下幾步:

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

  2)若是在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,若是經過則返回這個方法的直接引用,查找結束;不經過則返回java.lang.IllegalAccessError異常。

  3)不然,按繼承關係從下往上依次對C的各個父類將那些第二步的搜索和校驗

  4)若是始終沒找到合適的方法,則拋出java.lang.AbstractMethodError.

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

package com.ryj.hotspot.dispatch;

public class Test {
    public static void main(String[] args) {
        System.out.println(calc());
    }

    private static int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a+b)*c;
    }
}

 

 0: bipush        100         :執行偏移地址爲0的指令,Bipush指令的做用是將單字節的整型常量 100 推入操做數棧頂

 2: istore_0                     :istore_0指令的做用是將操做數棧頂的整型值出棧並存放到第0個局部變量槽

 11: iload_0                    :iload_0指令的做用是將局部變量表第0個變量槽中的整型值複製到操做數棧頂

 13: iadd                         :iadd指令的做用是將操做數棧中的頭兩個棧頂元素出棧,作整型加法,而後把結果入棧

相關文章
相關標籤/搜索