6、第三部分 虛擬機執(zhí)行子系統(tǒng)-第8章 虛擬機字節(jié)碼執(zhí)行引擎

概述

虛擬機與物理機的執(zhí)行引擎區(qū)別:

  • 物理機執(zhí)行引擎:直接建立在處理器、緩存、指令集和操作系統(tǒng)層面上的,可以近似理解為基于硬件的
  • 虛擬機執(zhí)行引擎:由軟件自行實現(xiàn),因此可以不受物理條件制約地定制指令集與執(zhí)行引擎的結構體系,能夠執(zhí)行哪些不被硬件直接支持的指令集格式。

運行時棧幀結構

Java虛擬機以方法作為最基本的執(zhí)行單元,“棧幀”(Stack Frame)則是用于支持虛擬機進行方法調用和方法執(zhí)行背后的數(shù)據(jù)結構,它也是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧(Virtual Machine Stack)的棧元素。棧幀存儲了方法的局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息。每一個方法從調用開始至執(zhí)行結束的過程,都對應著一個棧幀在虛擬機棧里面從入棧到出棧的過程。

每一個棧幀都包括了局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址和一些額外的附加信息。在編譯時所需的局部變量表大小和操作數(shù)棧就已經(jīng)確定。
在活動線程中,只有位于棧頂?shù)姆椒ú攀窃谶\行的,只有位于棧頂?shù)臈攀巧У?,其被稱為“當前棧幀”(Current Stack Frame),與 這個棧幀所關聯(lián)的方法被稱為“當前方法”(Current Method)。執(zhí)行引擎所運行的所有字節(jié)碼指令都只針對當前棧幀進行操作。


棧幀的概念結構

局部變量表

局部變量表的容量以變量槽(Variable Slot)為最小單位。一個變量槽可以存放一個數(shù)據(jù)類型。局部變量表中的變量槽是可以重用的。
由于局部變量表是建立在線程堆棧中的,屬于線程私有的數(shù)據(jù),無論讀寫兩個連續(xù)的變量槽是否為原子操作,都不會引起數(shù)據(jù)競爭和線程安全問題。
Java虛擬機通過索引定位的方式使用局部變量表,索引值的范圍是從0開始至局部變量表最大的變量槽數(shù)量。32位數(shù)據(jù)類型的變量,索引N就代表了使用第N個變量槽,64位數(shù)據(jù)類型的變量,同時使用第N和N+1兩個變量槽。對于兩個相鄰的共同存放一個64位數(shù)據(jù)的兩個變量槽,虛擬機不允許采用任何方式單獨訪問其中的某一個。

當一個方法被調用時,即實參到形參的傳遞。如果執(zhí)行的是實例方法(沒有被static修飾的方法),那局部變量表中第0位索引的變量槽默認是用于傳遞方法所屬對象實例的引用,即為了方法中使用this來表示實例引用。其余參數(shù)則按照參數(shù)表順序排列,占用從1開始的局部變量槽,參數(shù)表分配完畢后,再根據(jù)方法體內部定義的變量順序和作用域分配其余的變量槽。

操作數(shù)棧

操作數(shù)棧(Operand Stack)稱為操作棧,是后入先出棧。操作數(shù)棧的深度在編譯的時候被寫入到Code屬性的max_stacks數(shù)據(jù)項中。32位數(shù)據(jù)類型的棧容量是1,64位數(shù)據(jù)類型所占的棧容量是2。

方法的執(zhí)行過程最終體現(xiàn)的是執(zhí)行一系列出棧和入棧的操作指令過程。

在概念模型中,兩個不同棧幀作為不同的方法的虛擬機棧的元素,是完全相互獨立的。但是在大多數(shù)虛擬機實現(xiàn)做了優(yōu)化,另兩個棧幀出現(xiàn)一部分重疊讓下面棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起,這樣做不僅節(jié)約了一些空間,更重要的是在進行方法調用時就可以直接共用一部分數(shù)據(jù),無須進行額外的參數(shù)復制傳遞了。

Java虛擬機的解釋執(zhí)行引擎被稱為“基于棧的執(zhí)行引擎”,里面的“?!本褪遣僮鲾?shù)棧。


動態(tài)連接

每個棧幀都包含一個指向運行時常量中該棧幀所屬方法的引用持有這個引用是為了支持方法調用過程中的動態(tài)連接(Dynamic Linking)。字節(jié)碼中常量池中的符號引用,一部分在類加載或第一次使用就被轉為直接引用,稱為靜態(tài)解析。另一部分將在每次運行期間都轉為直接引用,稱為動態(tài)連接。

方法返回地址

方法的執(zhí)行只有兩種方式退出方法:

  • 正常調用完成(Normal Method Invocation Completion):執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令。
  • 異常調用完成(Abrupt Method Invocation Completion):方法執(zhí)行的過程中遇到了異常,并且這個異常沒有在方法體內得到妥善處理,不會給它的上層調用者提供任何返回值的。
    在方法退出之后,都必須返回到最初方法被調用時的位置,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層主調方法的執(zhí)行狀態(tài)。 一般來說,方法正常退出時,主調方法的PC計數(shù)器的值就可以作為返回地址,棧幀中很可能會保存這個計數(shù)器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中就一般不會保存這部分信息。

方法退出的過程實際上等同于把當前棧幀出棧,因此退出時可能執(zhí)行的操作有:恢復上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調用者棧幀的操作數(shù)棧中,調整PC計數(shù)器的值以指向方法調用指令后面的一條指令等。

方法調用

方法調用階段唯一的任務就是確定被調用方法的版本 (即調用哪一個方法),暫時還未涉及方法內部的具體運行過程。

解析

所有方法調用的目標方法在Class文件里面都是一個常量池中的符號引用,在類加載的解析階段,會將部分符號引用轉化為直接引用,前提是:方法在程序執(zhí)行前后的調用版本是不可改變的。這類方法的調用叫解析(Resolution)。

適合類加載階段進行解析的有靜態(tài)方法和私有方法兩大類,前者與類型直接關聯(lián),后者在外部不可被訪問。
不同方法調用字節(jié)碼指令:

  • invokestatic。用于調用靜態(tài)方法。
  • invokespecial。用于調用實例構造器<init>()方法、私有方法和父類中的方法。
  • invokevirtual。用于調用所有的虛方法。
  • invokeinterface。用于調用接口方法,會在運行時再確定一個實現(xiàn)該接口的對象。
  • invokedynamic。先在運行時動態(tài)解析出調用點限定符所引用的方法,然后再執(zhí)行該方法。前面4 條調用指令,分派邏輯都固化在Java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶設定的引導方法來決定的。
    非虛方法(Non-Virtual Method):調用的方法都可以在解析階段中確定唯一的調用版本,Java中符合的方法有:靜態(tài)方法、私有方法、實例構造器、父類方法和被final修飾的方法,在類加載時把符號引用解析為直接引用。反之其他的都被稱為虛方法(Virtual Method)
/**
 * 方法靜態(tài)解析演示
 */
public class StaticResolution {
    public static void sayHello() {
        System.out.println("hello world!");
    }

    public static void main(String[] args) {
        StaticResolution.sayHello();
    }
}
//調用的方法在編譯就明確以常量池項的形式固化在字節(jié)碼指令參數(shù)中(#5)
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #5                 // Method sayHello:()V
         3: return
      LineNumberTable:
        line 9: 0
        line 10: 3

解析調用一定是個靜態(tài)的過程,在編譯期間就完全確定。

另一種主要的方法調用形式:分派調用(Dispatch),可能是靜態(tài)或者動態(tài)的,按分派依據(jù)的宗量數(shù)可分為單分派和多分派,兩兩組合構成:靜態(tài)單分派、靜態(tài)多分派、動態(tài)單分派、動態(tài)多分派。

分派

  1. 靜態(tài)分派
/**
 * 方法靜態(tài)分派演示
 * 輸出結果是:
 * hello, guy!
 * hello, guy!
 */
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("hello, guy!");
    }

    public void sayHello(Man man) {
        System.out.println("hello, gentleman!");
    }

    public void sayHello(Woman man) {
        System.out.println("hello, lady!");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

Human稱為變量的“靜態(tài)類型”(Static Type)或者“外觀類型”(Apparent Type),后面的Man稱為變量的“實際類型”(Actual Type)或者叫“運行時類型”(Runtime Type)。靜態(tài)類型和實際類型在程序中都可能會發(fā)生變化,區(qū)別是靜態(tài)類型的變化僅僅在使用時發(fā)生,變量本身的靜態(tài)類型不會被改變,并且最終的靜態(tài)類型是在編譯期可知的;而實際類型變化的結果在運行期才可確定。

在方法接收者已經(jīng)確定是對象“sr”的前提下,使用哪個重載版本,就完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型。代碼中故意定義了兩個靜態(tài)類型相同,而實際類型不同的變量,但虛擬機(或者準確地說是編譯器)在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為 判定依據(jù)的。由于靜態(tài)類型在編譯期可知,所以在編譯階段,Javac編譯器就根據(jù)參數(shù)的靜態(tài)類型決定 了會使用哪個重載版本,因此選擇了sayHello(Human)作為調用目標,并把這個方法的符號引用寫到 main()方法里的兩條invokevirtual指令的參數(shù)中。

所有依賴靜態(tài)類型來決定方法執(zhí)行版本的分派動作,都稱為靜態(tài)分派。\color{red}{靜態(tài)分派的最典型應用表現(xiàn)就是方法重載。}

如果方法重載有多個版本,會確定一個相對更合適的版本。

  1. /**
     *注釋sayHello(char arg)方法
     *輸出結果
     *hello int
     *發(fā)生了自動類型轉換,'a'除了可以代表一個字符串,還可以代表數(shù)字97
     */
    
    /**
     *繼續(xù)注釋掉say Hello(int arg)方法
     *輸出結果
     *hello long
     *發(fā)生了兩次自動類型轉換,'a'轉型為整數(shù)97之后,進一步轉型為長整數(shù)97L,匹配了參數(shù)類型 為long的重載。
     實際還能發(fā)生多次自動轉型,按照char>int>long>float>double順序
     */
    
    /**
     *注釋掉sayHello(long arg)方法
     *輸出結果
     *hello Character
     *發(fā)生了一次自動裝箱,'a'被包裝為它的封裝類型java.lang.Character,所以匹配到了參數(shù)類型為 Character的重載
     */
    
    /**
     *繼續(xù)注釋掉sayHello(Character arg)方法
     *輸出結果
     *hello Serializable
     *因為java.lang.Serializ able是java.lang.Charact er類實現(xiàn)的一個接口,當自動裝箱之后發(fā)現(xiàn)還是找不到裝 箱類,但是找到了裝箱類所實現(xiàn)的接口類型,所以緊接著又發(fā)生一次自動轉型。char可以轉型成int, 但是Charact er是絕對不會轉型為Int eger的,它只能安全地轉型為它實現(xiàn)的接口或父類。
     */
    
    /**
     *繼續(xù)注釋掉sayHello(Serializable arg)方法
     *輸出結果
     *hello Object
     *char裝箱后轉型為父類了,如果有多個父類,那將在繼承關系中從下往上開始搜索,越接 上層的優(yōu)先級越低。
     */
    
    /**
     *say Hello(Object arg)也注釋掉
     *輸出結果
     *hello char ...
     *優(yōu)先級最低
     */
    
  2. 動態(tài)分派
    \color{red}{Java語言里動態(tài)分派與多態(tài)性的重寫(Override)有著很密切的關聯(lián)。}

/**
 *方法動態(tài)分派演示
 *輸出結果
 * man say hello
 * woman say hello
 * woman say hello
 */
public class DynamicDispatch {
    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("man say hello");
        }
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("woman say hello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class part8/DynamicDispatch$Man
         3: dup
         4: invokespecial #3                  // Method part8/DynamicDispatch$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class part8/DynamicDispatch$Woman
        11: dup
        12: invokespecial #5                  // Method part8/DynamicDispatch$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method part8/DynamicDispatch$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method part8/DynamicDispatch$Human.sayHello:()V
        24: new           #4                  // class part8/DynamicDispatch$Woman
        27: dup
        28: invokespecial #5                  // Method part8/DynamicDispatch$Woman."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method part8/DynamicDispatch$Human.sayHello:()V
        36: return

0~15的字節(jié)碼是準備動作,作用是建立man和woman內存空間、調用Man和Woman類型的實例構造器,將兩個實例引用存放在第1、2個局部變量表的變量槽中,對應代碼:

Human man = new Man();
Human woman = new Woman();

16~21的字節(jié)碼是將創(chuàng)建的兩個對象壓入棧頂,兩條invokevirtual引用的都是Human.sayHello,但是最終執(zhí)行的目標方法是不同的。invokevirtual指令的運行時解析過程大致分為:

  1. 找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記作C。
  2. 如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;不通過則返回java.lang.IllegalAccessError異常。
  3. 否則,按照繼承關系從下往上依次對C的各個父類進行第二步的搜索和驗證過程。
  4. 如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。
    正是因為invokevirtual指令執(zhí)行的第一步就是在運行期確定接收者的實際類型,所以兩次調用中的 invokevirtual指令并不是把常量池中方法的符號引用解析到直接引用上就結束了,還會根據(jù)方法接收者的實際類型來選擇方法版本,這個過程就是Java語言中方法重寫的本質。我們把這種在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派。
/**
 * 字段不參與多態(tài)
 * 輸出結果
 * I am Son, i have $0
 * I am Son, i have $4
 * This gay has $2
 * 當子類聲明了與父類同名的字段時,雖然在子類的內存中兩個字段都會存在,但是子類的字段會遮蔽父類的同名字段。
 * 輸出兩句都是“I am Son”,這是因為Son類在創(chuàng)建的時候,首先隱式調用了Father的構造函數(shù),而 Father構造函數(shù)中對showMeTheMoney()
 * 的調用是一次虛方法調用,實際執(zhí)行的版本是 Son::showMeTheMoney()方法,所以輸出的是“I am Son”。而這時候雖然父類的money字段已
 * 經(jīng)被初始化成2了,但Son::showMeTheMoney()方法中訪問的卻是子類的money 字段,這時候結果自然還是0,因為它要到子類的構造函數(shù)執(zhí)行
 * 時才會被初始化。 main()的最后一句通過靜態(tài)類型訪問到了父類中的money ,輸出了2。
 */
public class FieldHasNoPolymorphic {
    static class Father {
        public int money = 1;

        public Father() {
            money = 2;
            showMeTheMoney();
        }

        public void showMeTheMoney() {
            System.out.println("I am Father, i have $" + money);
        }
    }

    static class Son extends Father {
        public int money = 3;

        public Son() {
            money = 4;
            showMeTheMoney();
        }

        public void showMeTheMoney() {
            System.out.println("I am Son, i have $" + money);
        }
    }

    public static void main(String[] args) {
        Father gay = new Son();
        System.out.println("This gay has $" + gay.money);
    }
}
  1. 單分派與多分派
    方法的接收者與方法的參數(shù)統(tǒng)稱為方法的宗量。根據(jù)分派基于多少種宗量,可以將分派劃分為單分派和多分派兩種。單分派是根據(jù)一個宗量對目標方法進行選擇,多分派則是根據(jù)多于一個宗量對目標方法進行選擇。



    運行階段中虛擬機的選擇,也就是動態(tài)分派的過程。在執(zhí)行“son.hardChoice(newQQ())”這行代碼時,更準確地說,是在執(zhí)行這行代碼所對應的invokevirtual指令時,由于編譯期已經(jīng)決定目標方法的簽名必須為hardChoice(QQ),虛擬機此時不會關心傳遞過來的參數(shù)“QQ”到底是“騰訊QQ”還是“奇瑞QQ”,因為這時候參數(shù)的靜態(tài)類型、實際類型都對方法的選擇不會構成任何影響,唯一可以影響虛擬機選擇的因素只有該方法的接受者的實際類型是Father還是Son。因為只有一個宗量作為選擇依據(jù),所以Java語言的動態(tài)分派屬于單分派類型。

Java語言是一門靜態(tài)多分派,動態(tài)單分派語言。

  1. 虛擬機動態(tài)分派的實現(xiàn)
    而且動態(tài)分派的方法版本選擇過程需要運行時在接收者類型的方法元數(shù)據(jù)中搜索合適的目標方法,因此,Java虛擬機面對這種情況,一種基礎而且常見的優(yōu)化手段是為類型在方法區(qū)中建立一個虛方法表(Virtual Method Table,也稱為vtable,與此對應的,在invokeinterface執(zhí)行時也會用到接口方法表——Interface Method Table,簡稱itable),使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能


    方法表結構

    虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表中的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現(xiàn)入口。如果子類重寫了這個方法,子類虛方法表的地址也會被替換成執(zhí)行子類實現(xiàn)版本的入口地址。上圖的hardChoice方法父類和子類指向的地址是不一樣的。

為了程序實現(xiàn)方便,具備相同簽名的父類、子類的虛方法表中具有一樣的索引序號,當類型轉換時,只需變更查找的虛方法表即可。虛方法表一般在類加載的連接階段進行初始化,準備了類的變量初始值后,虛擬機會把該類的虛方法表也一同初始化完畢。

動態(tài)類型語言支持

動態(tài)類型語言

動態(tài)類型語言:動態(tài)類型語言的關鍵特征是它的類型檢查的主體過程是在運行期而不是編譯期進行的。而Java是在編譯期就進行類型檢查過程的語言,所以是靜態(tài)類型語言。

public static void main(String[] args) { 
    int[][][] array = new int[1][0][-1];
}

上面代碼能正常編譯,但是運行會出現(xiàn)NegativeArraySizeException運行時異常,是在運行期拋出的。相反是連接時異常,例如NoClassDefFoundError,即導致連接時異常的代碼放在一條根本無法被執(zhí)行的路徑分支上,類加載時也會照樣拋出異常。

什么是類型檢查?

obj.println("hello world");

假設上面一行是Java語言,并且變量obj的靜態(tài)類型為java.io.PrintStream,那變量obj的實際類型就必須是PrintStream的子類(實現(xiàn)PrintStream接口的類)才是合法的。否則,哪怕obj屬于一個確定包含有println(String)方法相同簽名方法的類型,但只要它與PrintStream接口沒有繼承關系,代碼依然不可能運行——因為類型檢查不合法。

但是相同的代碼在ECMAScript(JavaScript)中情況則不一樣,無論obj具體是何種類型,無論其繼承關系如何,只要這種類型的方法定義中確實包含有println(String)方法,能夠找到相同簽名的方法,調用便可成功。

產(chǎn)生這種差別產(chǎn)生的根本原因是Java語言在編譯期間卻已將println(String)方法完整的符號引用(本例中為一項CONSTANT_InterfaceMethodref_info常量)生成出來,并作為方法調用指令的參數(shù)存儲到 Class文件中。例如:

invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V

這個符號引用包括了該方法定義在哪個具體類型之中、方法的名字以及參數(shù)順序、參數(shù)類型和方 法返回值等信息,通過這個符號引用,Java虛擬機就可以翻譯出該方法的直接引用。而ECMAScript等 動態(tài)類型語言與Java有一個核心的差異就是變量obj本身并沒有類型,變量obj的值才具有類型,所以編譯器在編譯時最多只能確定方法名稱、參數(shù)、返回值這些信息,而不會去確定方法所在的具體類型 (即方法接收者不固定)。變量無類型而變量值才有類型這個特點也是動態(tài)類型語言的一個核心特征。

靜態(tài)類型語言與動態(tài)類型語言的比較:

  • 靜態(tài)類型語言:能夠在編譯期確定變量類型,編譯期可以提供全面嚴謹?shù)念愋蜋z查,這樣與數(shù)據(jù)類型相關的潛在問題就能在編碼時被及時發(fā)現(xiàn),利于穩(wěn)定性及讓項目容易達到更大的規(guī)模。
  • 動態(tài)類型語言:在運行期才確定類型,靈活性,某些在靜態(tài)類型語言中要花大量臃腫代碼來實現(xiàn)的功能,由動態(tài)類型語言去做可能會很清晰簡潔。

Java與動態(tài)類型

java.lang.invoke包

JDK7新加入java.lang.invoke,這個包的主要目的是在之前單純依靠符號引用確定調用的目標方法之外,提供一種新的動態(tài)確定目標方法的機制,稱為方法句柄(Method Handle)。

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

import static java.lang.invoke.MethodHandles.lookup;

/**
 *方法句柄演示
 */
public class MethodHandleTest {
    static class ClassA {
        public void println(String s) {
            System.out.println(s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        // 無論obj最終是哪個實現(xiàn)類,下面這句都能正確調用到println方法。
        getPrintlnMH(obj).invokeExact("icyfenix");
    }

    //模擬了invokevirtual指令的執(zhí)行過程
    private static MethodHandle getPrintlnMH(Object reveiver) throws NoSuchMethodException, IllegalAccessException {
        // MethodType:代表“方法類型”,包含了方法的返回值(methodType()的第一個參數(shù))和具體參數(shù)(methodType()第二個及以后的參數(shù))。
        MethodType mt = MethodType.methodType(void.class, String.class);
        // lookup()方法來自于MethodHandles.lookup,這句的作用是在指定類中查找符合給定的方法名稱、方法類型,并且符合調用權限的方法句柄。
        // 因為這里調用的是一個虛方法,按照Java語言的規(guī)則,方法第一個參數(shù)是隱式的,代表該方法的接收者,也即this指向的對象,這個參數(shù)以前是放在參數(shù)列表中進行傳遞,現(xiàn)在提供了bindTo()方法來完成這件事情。
        return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    }
}

MethodHandle在使用方法和效果上與Reflection有很多類似的地方,但是還是有區(qū)別:

站在Java語言的角度看:

  • MethodHandle和Reflection機制本質上都是在模擬方法調用。但是Reflection是在模擬Java代碼層次的方法調用,而MethodHandle是在模擬字節(jié)碼層次的方法調用。在MethodHandles.Lookup上的3個方法findStatic()、findVirtual()、findSpecial()正是為了對應于invokestatic、invokevirtual(以及invokeinterface和invokespecial這幾條字節(jié)碼指令的執(zhí)行權限校驗行為,而這些底層細節(jié)在使用ReflectionAPI時是不需要關心的。
  • Reflection中的java.lang.reflect.Method對象遠比MethodHandle機制中的
    java.lang.invoke.MethodHandle對象所包含的信息來得多。前者是方法在Java端的全面映像,包含了方法的簽名、描述符以及方法屬性表中各種屬性的Java端表示方式,還包含執(zhí)行權限等的運行期信息。而后者僅包含執(zhí)行該方法的相關信息。用開發(fā)人員通俗的話來講,Reflection是重量級,而MethodHandle是輕量級。
  • 由于MethodHandle是對字節(jié)碼的方法指令調用的模擬,那理論上虛擬機在這方面做的各種優(yōu)化(如方法內聯(lián)),在MethodHandle上也應當可以采用類似思路去支持(但目前實現(xiàn)還在繼續(xù)完善中),而通過反射去調用方法則幾乎不可能直接去實施各類調用點優(yōu)化措施。

不止于Java語言的角度看:

Reflection API的設計目標是只為Java語言服務的,而MethodHandle 則設計為可服務于所有Java虛擬機之上的語言,其中也包括了Java語言而已。

invokedynamic指令

每一處含有invokedynamic指令的位置都被稱作“動態(tài)調用點(Dynamically-Computed Call Site)”。這條指令的第一個參數(shù)不再是代表方法符號引用的CONSTANT_Methodref_info常量,而是變?yōu)镴DK 7時新加入的CONSTANT_InvokeDynamic_info常量,從這個新常量中可以得到3項信息:引導方法 (Bootstrap Method,該方法存放在新增的BootstrapMethods屬性中)、方法類型(MethodType)和名稱。引導方法是有固定的參數(shù),并且返回值規(guī)定是java.lang.invoke.CallSite對象,這個對象代表了真正要執(zhí)行的目標方法調用。根據(jù)CONSTANT_InvokeDynamic_info常量中提供的信息,虛擬機可以找到并且執(zhí)行引導方法,從而獲得一個CallSite對象,最終調用到要執(zhí)行的目標方法上。

基于棧的字節(jié)碼解釋執(zhí)行引擎

許多Java虛擬機的執(zhí)行引擎在執(zhí)行Java代碼的時候都有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇。

解釋執(zhí)行

大部分的程序代碼轉換成物理機的目標代碼或虛擬機能執(zhí)行的指令集之前,都需要經(jīng)過圖8-4中的各個步驟。


編譯過程

基于棧的指令集與基于寄存器的指令集

Javac編譯器輸出的字節(jié)碼指令流,基本上是一種基于棧的指令集架構(Instruction Set Architecture,ISA),字節(jié)碼指令流里面的指令大部分都是零地址指令,依賴操作數(shù)棧進行工作。與之相對的另外一套常用的指令集架構是基于寄存器的指令集(x86)

基于棧的指令集與基于寄存器的指令集區(qū)別:

//基于棧的指令集例子
iconst_1
iconst_1
iadd
istore_0
//基于寄存器的指令集例子
mov eax, 1
add eax, 1

基于棧的指令集主要優(yōu)點是可移植,因為寄存器由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免地要受到硬件的約束。代碼相對更加緊湊(字節(jié)碼中每個字節(jié)就對應一條指令,而多地址指令集中還需要存放參數(shù))、編譯器實現(xiàn)更加簡單(不需要考慮空間分配的問題,所需空間都在棧上操作)等。

在解釋執(zhí)行時,棧架構指令集的代碼雖然緊湊,但是完成相同功能所需的指令數(shù)量一般會比寄存器架構來得更多,因為出棧、入棧操作本身就產(chǎn)生了相當大量的指令。更重要的是棧實現(xiàn)在內存中, 頻繁的棧訪問也就意味著頻繁的內存訪問,相對于處理器來說,內存始終是執(zhí)行速度的瓶頸。盡管虛擬機可以采取棧頂緩存的優(yōu)化方法,把最常用的操作映射到寄存器中避免直接內存訪問,但這也只是優(yōu)化措施而不是解決本質問題的方法。因此由于指令數(shù)量和內存訪問的原因,導致了棧架構指令集的 執(zhí)行速度會相對慢上一點。

基于棧的解釋器執(zhí)行過程

public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    }
public int calc();
    Code:
        Stack=2, Locals=4, Args_size=1
        0: bipush 100
        2: istore_1
        3: sipush 200
        6: istore_2
        7: sipush 300 10: istore_3
        11: iload_1
        12: iload_2
        13: iadd 14: iload_3 15: imul 16: ireturn
}

javap 提示這段代碼需要深度為2的操作數(shù)棧和4個變量槽的局部變量空間。









上面的執(zhí)行過程僅僅是一種概念模型,虛擬機最終會對執(zhí)行過程做出一系列優(yōu)化來提高性能,實際情況會和上面描述的概念模型差距非常大,差距產(chǎn)生的根本原因是虛擬機中解析器和即時編譯器都會對輸入的字節(jié)碼進行優(yōu)化,即使解釋器中也不是按照字節(jié)碼指令去逐條執(zhí)行的。例如在HotSpot虛擬機中,就有很多以“fast_”開頭的非標準字節(jié)碼指令用于合并、替換輸入的字節(jié)碼以提升解釋執(zhí)行性能,即時編譯器的優(yōu)化手段則更是花樣繁多

源自書籍:深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)-周志明

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯(lián)系作者。

相關閱讀更多精彩內容

友情鏈接更多精彩內容