第八章 虛擬機(jī)字節(jié)碼執(zhí)行引擎

[目錄]
概述

1 概述

  • 不同的虛擬機(jī)實現(xiàn)里面,執(zhí)行引擎在執(zhí)行Java代碼的時候可能會有解釋執(zhí)行通過解釋器執(zhí)行)和編譯執(zhí)行通過即時編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇也可能兩者兼?zhèn)?/li>
  • 從外觀上看起來,所有的Java虛擬機(jī)的執(zhí)行引擎都是一致的:輸入的是字節(jié)碼文件處理過程字節(jié)碼解析的等效過程輸出的是執(zhí)行結(jié)果
  • 本章將主要從概念模型的角度來講解虛擬機(jī)的方法調(diào)用和字節(jié)碼執(zhí)行。

2 運(yùn)行時棧幀結(jié)構(gòu)

  • 棧幀(Stack Frame)是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)中的虛擬機(jī)棧(Virtual Machine Stack)的棧元素,用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)

  • 存儲了方法的局部變量表操作數(shù)棧動態(tài)連接方法返回地址等信息

  • 每一個方法從調(diào)用開始至執(zhí)行完成的過程 **---------> **一個棧幀在虛擬機(jī)棧里面從入棧到出棧的過程

  • 編譯程序代碼的時候,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到方法表的Code屬性之中

  • 一個線程中的方法調(diào)用鏈可能會很長,很多方法都同時處于執(zhí)行狀態(tài)(方法間互相調(diào)用)

  • 對于執(zhí)行引擎來說,在活動線程中,只有位于棧頂的棧幀才是有效的,稱為當(dāng)前棧幀(Current StackFrame),與這個棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)

2.1 局部變量表

  • 局部變量表(Local Variable Table)是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在Java程序編譯為Class文件時,就在方法的Code屬性max_locals數(shù)據(jù)項中確定了該方法所需要分配的局部變量表的最大容量。
  • 局部變量表的容量以變量槽(Variable Slot,下稱Slot)為最小單位
  • 一個Slot可以存放一個32位以內(nèi)的數(shù)據(jù)類型,Java中占用32位以內(nèi)的數(shù)據(jù)類型有booleanbytecharshortintfloatreferencereturnAddress種類型
  • reference類型表示對一個對象實例的引用,一是從此引用中直接或間接地查找到對象在Java堆中的數(shù)據(jù)存放的起始地址索引,二是此引用中直接或間接地查找到對象所屬數(shù)據(jù)類型在方法區(qū)中的存儲的類型信息

2.2 操作數(shù)棧

  • 操作數(shù)棧(Operand Stack)也常稱為操作棧,它是一個后入先出(Last In First Out,LIFO)棧。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時候?qū)懭氲?code>Code屬性的max_stacks數(shù)據(jù)項中。操作數(shù)棧的每一個元素可以是任意的Java數(shù)據(jù)類型,包括long和double。32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2。在方法執(zhí)行的任何時候,操作數(shù)棧的深度都不會超過在max_stacks數(shù)據(jù)項中設(shè)定的最大值。

2.3 動態(tài)連接

  • 每個棧幀都包含一個指向運(yùn)行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接(Dynamic Linking)

2.4 方法返回地址

兩種返回的方法:
正常完成出口(Normal Method Invocation Completion)異常完成出口(Abrupt Method Invocation Completion)
無論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來說,方法正常退出時,調(diào)用者的PC計數(shù)器的值可以作為返回地址,棧幀中很可能會保存這個計數(shù)器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息

3 方法調(diào)用

方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個方法),暫時還不涉及方法內(nèi)部的具體運(yùn)行過程。

3.1 解析

  • 調(diào)用目標(biāo)在程序代碼寫好、編譯器進(jìn)行編譯時就必須確定下來。這類方法的調(diào)用稱為解析(Resolution)
  • 第七章講過:目標(biāo)方法在Class文件里面都是一個常量池中的符號引用,在類加載的解析階段,會將其中的一部分符號引用轉(zhuǎn)化為直接引用,這
    種解析能成立的前提是方法在程序真正運(yùn)行之前就有一個可確定的調(diào)用版本,并且這個方法的調(diào)用版本在運(yùn)行期是不可改變的
  • 符合“編譯期可知,運(yùn)行期不可變”這個要求的方法,主要包括靜態(tài)方法私有方法兩大類

Java虛擬機(jī)里面提供了5條方法調(diào)用字節(jié)碼指令

方法名 解釋 備注
invokestatic 調(diào)用靜態(tài)方法
invokespecial 調(diào)用實例構(gòu)造器<init>方法、私有方法和父類方法
invokevirtual 調(diào)用所有的虛方法
invokeinterface 調(diào)用接口方法,會在運(yùn)行時再確定一個實現(xiàn)此接口的對象
invokedynamic todo todo

只要能被invokestaticinvokespecial指令調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本,符合這個條件的有靜態(tài)方法私有方法實例構(gòu)造器父類方法4類,它們在類加載的時候就會把符號引用解析為該方法的直接引用


invokestaticinvokespecial指令調(diào)用的方法 稱為非虛方法,與之相
反,其他方法稱為虛方法(除去final方法,后文會提到)

3.2 分派

3.2.1 靜態(tài)分派

  • 定義: 依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作
  • 靜態(tài)分派發(fā)生在編譯階段
  • 確定靜態(tài)分派的動作實際上不是由虛擬機(jī)來執(zhí)行的。

示例代碼

package study8;

/**
 * Created by haicheng.lhc on 05/04/2017.
 *
 * @author haicheng.lhc
 * @date 2017/04/05
 */
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 guy) {
        System.out.println("hello,gentleman!");
    }

    public void sayHello(Woman guy) {
        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);
    }
}

結(jié)果分析:

我們把上面代碼中的“Human”稱為變量的靜態(tài)類型(Static Type),或者叫做的外觀類型(Apparent Type),后面的“Man”則稱為變量的實際類型(Actual Type),靜態(tài)類型和實際類型在程序中都可以發(fā)生一些變化,區(qū)別是靜態(tài)類型的變化僅僅在使用時發(fā)生變量本身的靜態(tài)類型不會被改變,并且最終的靜態(tài)類型是在編譯期可知的;而實際類型變化的結(jié)果在運(yùn)行期才可確定,編譯器在編譯程序的時候并不知道一個對象的實際類型是什么.虛擬機(jī)(準(zhǔn)確地說是編譯器)在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)的

3.2.2.動態(tài)分派

  • (Override)有著很密切的關(guān)聯(lián)

示例代碼

package study8;

/**
 * Created by haicheng.lhc on 05/04/2017.
 *
 * @author haicheng.lhc
 * @date 2017/04/05
 */
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();
    }
}

解析
使用javap分析

Compiled from "DynamicDispatch.java"
public class study8.DynamicDispatch extends java.lang.Object
  SourceFile: "DynamicDispatch.java"
  InnerClass:
   #9= #4 of #7; //Woman=class study8/DynamicDispatch$Woman of class study8/DynamicDispatch
   #11= #2 of #7; //Man=class study8/DynamicDispatch$Man of class study8/DynamicDispatch
   abstract #13= #12 of #7; //Human=class study8/DynamicDispatch$Human of class study8/DynamicDispatch
  minor version: 0
  major version: 50
  Constant pool:
const #1 = Method   #8.#22; //  java/lang/Object."<init>":()V
const #2 = class    #23;    //  study8/DynamicDispatch$Man
const #3 = Method   #2.#22; //  study8/DynamicDispatch$Man."<init>":()V
const #4 = class    #24;    //  study8/DynamicDispatch$Woman
const #5 = Method   #4.#22; //  study8/DynamicDispatch$Woman."<init>":()V
const #6 = Method   #12.#25;    //  study8/DynamicDispatch$Human.sayHello:()V
const #7 = class    #26;    //  study8/DynamicDispatch
const #8 = class    #27;    //  java/lang/Object
const #9 = Asciz    Woman;
const #10 = Asciz   InnerClasses;
const #11 = Asciz   Man;
const #12 = class   #28;    //  study8/DynamicDispatch$Human
const #13 = Asciz   Human;
const #14 = Asciz   <init>;
const #15 = Asciz   ()V;
const #16 = Asciz   Code;
const #17 = Asciz   LineNumberTable;
const #18 = Asciz   main;
const #19 = Asciz   ([Ljava/lang/String;)V;
const #20 = Asciz   SourceFile;
const #21 = Asciz   DynamicDispatch.java;
const #22 = NameAndType #14:#15;//  "<init>":()V
const #23 = Asciz   study8/DynamicDispatch$Man;
const #24 = Asciz   study8/DynamicDispatch$Woman;
const #25 = NameAndType #29:#15;//  sayHello:()V
const #26 = Asciz   study8/DynamicDispatch;
const #27 = Asciz   java/lang/Object;
const #28 = Asciz   study8/DynamicDispatch$Human;
const #29 = Asciz   sayHello;

{
public study8.DynamicDispatch();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #1; //Method java/lang/Object."<init>":()V
   4:   return
  LineNumberTable:
   line 9: 0
   line 22: 4


public static void main(java.lang.String[]);
  Code:
   Stack=2, Locals=3, Args_size=1
   0:   new #2; //class study8/DynamicDispatch$Man
   3:   dup
   4:   invokespecial   #3; //Method study8/DynamicDispatch$Man."<init>":()V
   7:   astore_1
   8:   new #4; //class study8/DynamicDispatch$Woman
   11:  dup
   12:  invokespecial   #5; //Method study8/DynamicDispatch$Woman."<init>":()V
   15:  astore_2
   16:  aload_1
   17:  invokevirtual   #6; //Method study8/DynamicDispatch$Human.sayHello:()V
   20:  aload_2
   21:  invokevirtual   #6; //Method study8/DynamicDispatch$Human.sayHello:()V
   24:  new #4; //class study8/DynamicDispatch$Woman
   27:  dup
   28:  invokespecial   #5; //Method study8/DynamicDispatch$Woman."<init>":()V
   31:  astore_1
   32:  aload_1
   33:  invokevirtual   #6; //Method study8/DynamicDispatch$Human.sayHello:()V
   36:  return
  LineNumberTable:
   line 30: 0
   line 31: 8
   line 32: 16
   line 33: 20
   line 34: 24
   line 35: 32
   line 36: 36


}

雖然17 21 語句完全一樣,但是結(jié)果卻不一樣,原因是:從invokevirtual指令的多態(tài)查找過程開始說起,invokevirtual指令的運(yùn)行時解析過程大致分為以下幾個步驟:

1)找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記作C。
2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進(jìn)行訪問權(quán)限校驗,如果通過則返回這個方法的直接引用,查找過程結(jié)束;如果不通過,則返回java.lang.IllegalAccessError異常。
3)否則,按照繼承關(guān)系從下往上依次對C的各個父類進(jìn)行第2步的搜索和驗證過程。
4)如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。

3.2.3單分派與多分派

  • 方法的接收者與方法的參數(shù)統(tǒng)稱為方法的宗量,這個定義最早應(yīng)該來源于《Java與模式》一書。根據(jù)分派基于多少種宗量,可以將分派劃分為單分派和多分派兩種
  • 以Java語言的靜態(tài)分派屬于多分派類型
  • Java語言的動態(tài)分派屬于單分派類型

3.2.4 虛擬機(jī)動態(tài)分派的實現(xiàn)

  • 虛方法表(Vritual Method Table,也稱為vtable,與此對應(yīng)的,在invokeinterface執(zhí)行時也會用到接口方法表——Inteface Method Table,簡稱itable),使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。

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

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

  • 虛擬機(jī)是如何調(diào)用方法的內(nèi)容已經(jīng)講解完畢,從本節(jié)開始,我們來探討虛擬機(jī)是如何執(zhí)行方法中的字節(jié)碼指令的
  • 整個運(yùn)算過程的中間變量都以操作數(shù)棧的出棧、入棧為信息交換途徑
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容