JVM(六):虛擬機字節(jié)碼執(zhí)行引擎

在不同的虛擬機實現(xiàn)里面诚些,執(zhí)行引擎在執(zhí)行Java代碼的時候可能會有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時編譯器產(chǎn)生本地代碼執(zhí)行)兩種,也可能兩者兼?zhèn)洹?/p>

但從外觀上來看渴语,所有的Java虛擬機的執(zhí)行引擎都是一致的皮官,
輸入:字節(jié)碼文件
處理過程:字節(jié)碼解析的等效過程
輸出:執(zhí)行結(jié)果

一、運行時棧幀結(jié)構(gòu)

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

每個棧幀都包括了局部變量表季春、操作數(shù)棧洗搂、動態(tài)連接、方法返回地址和一些額外的附加信息。

1. 局部變量表

局部變量表是一組變量值的存儲空間耘拇,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量撵颊。

Java程序編譯為Class文件時,就可以在方法的Code屬性的max_locals項中確定該方法所需要分配的局部變量表的最大值惫叛。

局部變量表的容量以變量槽(Variable Slot倡勇,Slot)為最小單位。虛擬機規(guī)范中沒有明確指明一個Slot應占用的內(nèi)存大小嘉涌,但是每個Slot都應該能存放一個boolean妻熊、byte、char仑最、short扔役、int、float警医、reference或returnAddress類型的數(shù)據(jù)亿胸,這8種類型都可以使用32位以內(nèi)的物理內(nèi)存來存放。

reference類型表示對一個對象實例的引用预皇,需要滿足兩點:
(1)從此引用中直接或間接的查找到對象在Java堆中的數(shù)據(jù)存放的起始地址索引侈玄;
(2)從此引用中直接或間接的查找到對象所屬類型(類)在方法區(qū)中存儲的類型信息。

Java語言中明確的64位數(shù)據(jù)類型只有l(wèi)ong和double吟温,因此虛擬機會以高位對齊的方式為其分配兩個連續(xù)的Slot空間序仙。由于局部變量表建立在虛擬機棧中,是各線程私有的鲁豪,因此不存在線程安全問題潘悼。

如果執(zhí)行的是實例方法(非static方法),那么局部變量表的結(jié)構(gòu)如下:
(1)第0位索引的Slot默認是用于傳遞方法所屬對象實例的引用(即this所指的對象實例)呈昔。
(2)參數(shù)表按照順序排列挥等,從第1位索引開始直至參數(shù)表分配完畢。
(3)根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余Slot堤尾。
注意:為了盡可能節(jié)省棧幀空間,局部變量表中的Slot可以重用迁客。
如果當前字節(jié)碼PC計數(shù)器的值已經(jīng)超過了某個變量的作用域郭宝,那這個變量對應的Slot就可以被其他變量重用。

局部變量:不會初始化系統(tǒng)值掷漱,只會初始化程序員定義的值粘室。(必須初始化)
類變量:先初始化系統(tǒng)值,再初始化程序員定義的值卜范。(可以用系統(tǒng)初始值)

2. 操作數(shù)棧

操作數(shù)棧又稱操作棧衔统,它是一個后入先出的棧。
操作數(shù)棧的最大深度在編譯的時候已經(jīng)確定,寫入到Code屬性的max_stacks中锦爵。
操作數(shù)棧的每個元素可以是任意的Java數(shù)據(jù)類型舱殿,包括long和double,32位數(shù)據(jù)類型占用的棧容量為1险掀,64位數(shù)據(jù)類型占用的棧容量為2沪袭。

舉例:

public static void main(String[] args) {
    int a = 4;
    int b = 20;
    int c = a + b;
}

編譯:javac Test.java
查看字節(jié)碼:javap -verbose Test.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: iconst_4            // 將整型常量4加載到操作數(shù)棧頂
       1: istore_1            // 彈出操作數(shù)棧頂?shù)臄?shù)據(jù)并存儲到局部變量表Slot1處
       2: bipush      20      // 將整型常量20加載到操作數(shù)棧頂
       4: istore_2            // 彈出操作數(shù)棧頂?shù)臄?shù)據(jù)并存儲到局部變量表Slot2處
       5: iload_1             // 加載局部變量表Slot1處的數(shù)據(jù)到操作數(shù)棧頂
       6: iload_2             // 加載局部變量表Slot2處的數(shù)據(jù)到操作數(shù)棧頂
       7: iadd                  // 彈出操作數(shù)棧頂?shù)膬蓚€數(shù)據(jù)相加并將結(jié)果壓入操作數(shù)棧頂
       8: istore_3            // 彈出操作數(shù)棧頂?shù)臄?shù)據(jù)并存儲到局部變量表Slot3處
       9: return
3. 動態(tài)連接

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

4. 方法返回地址

當一個方法開始執(zhí)行后冈绊,只有兩種方式可以退出這個方法:
(1)正常完成出口:執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令。
(2)異常完成出口:執(zhí)行過程中遇到了異常埠啃,并且這個異常沒有在方法體內(nèi)得到處理死宣。
無論采用何種方式退出,在方法退出后碴开,都需要返回到方法被調(diào)用的位置十电,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在棧幀中保存一些信息叹螟,用來幫助恢復它的上層方法的執(zhí)行狀態(tài)鹃骂。

5. 附加信息

具體的虛擬機實現(xiàn)可以增加一些規(guī)范中沒有描述的信息到棧幀中。實際開發(fā)中罢绽,一般會把動態(tài)連接畏线、方法返回地址和附加信息歸為一類,稱為棧幀信息良价。

二寝殴、方法調(diào)用

方法調(diào)用階段的唯一任務就是確定被調(diào)用方法的版本(即調(diào)用哪一個方法),還不涉及方法內(nèi)部的具體運行過程明垢。

Class文件的編譯過程中不包含傳統(tǒng)編譯中的連接步驟蚣常,一切方法調(diào)用在Class文件里存儲的都是符號引用,而不是直接引用痊银。例如:invokevirtual #2 // Method add:()V抵蚊,其中#2就是符號引用,對應類常量池中的add()方法溯革。

1. 虛擬機解析

在類加載的解析階段贞绳,會將一部分方法調(diào)用中的目標方法的符號引用轉(zhuǎn)化為直接引用,這些目標方法需要滿足“編譯期可知致稀,運行期不可變”這個要求冈闭,這類方法的調(diào)用稱為解析。

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

  • invokestatic:調(diào)用靜態(tài)方法抖单。
  • invokespecial:調(diào)用實例構(gòu)造器<init>方法萎攒、私有方法和父類方法遇八。
  • invokevirtual:調(diào)用所有的虛方法。
  • invokeinterface:調(diào)用接口方法耍休,會在運行時再確定一個實現(xiàn)此接口的對象刃永。
  • invokedynamic:先在運行時動態(tài)解析出調(diào)用點限定符所引用的方法,然后再執(zhí)行該方法羹应。

只要能被invokestaticinvokespecial指令調(diào)用的方法揽碘,都可以在類加載的解析階段確定唯一的調(diào)用版本,把符號引用解析為該方法的直接引用园匹。這些方法被稱為非虛方法雳刺。
注意:final修飾的方法也是非虛方法。雖然final方法使用invokevirtual指令調(diào)用裸违,但是由于它無法被覆蓋掖桦,所以也無需對方法接收者進行多態(tài)選擇。

2. 虛擬機分派

靜態(tài)分派
對于代碼:Human man = new Man();
Human稱為變量的靜態(tài)類型供汛,在編譯期可知枪汪;
Man稱為變量的實際類型,在運行期才可確定怔昨。

所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作稱為靜態(tài)分派雀久。靜態(tài)分派發(fā)生在編譯階段,其實際動作不是由虛擬機執(zhí)行的趁舀。

靜態(tài)分派的典型應用是方法重載(Overload)赖捌,方法重載改變參數(shù)類型,此參數(shù)類型就是參數(shù)的靜態(tài)類型矮烹,在編譯階段越庇,javac編譯器就可以根據(jù)參數(shù)的靜態(tài)類型決定使用哪個更適合的重載版本了。

動態(tài)分派
在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派奉狈。

invokevirtual指令的多態(tài)查找過程:
(1)找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型卤唉,即為C。
(2)如果在類型C中找到與常量池中的描述符和簡單名稱都相符的方法仁期,則進行權(quán)限校驗桑驱,如果通過則返回這個方法的直接引用,查找結(jié)束蟀拷;校驗不通過拋出java.lang.IllegalAccessError碰纬。
(3)按照繼承關(guān)系從下往上依次對C的各個父類進行(2)的搜索驗證過程。
(4)如果始終沒有找到合適的方法问芬,則拋出java.lang.AbstractMethodError。

動態(tài)分派的典型應用是方法重寫(Override)寿桨,方法重寫的本質(zhì)就是:在運行期把常量池中的類方法符號引用解析為不同的直接引用此衅,從而選擇對應的重寫方法版本强戴。

單分派和多分派
執(zhí)行方法的所有者被稱為方法的接收者。
方法的接收者與方法的參數(shù)統(tǒng)稱為方法的宗量挡鞍。

如果根據(jù)一個宗量對目標方法進行選擇骑歹,稱為單分派。
如果根據(jù)多于一個宗量對目標方法進行選擇墨微,稱為多分派道媚。

Java語言的靜態(tài)分派根據(jù)方法接收者的靜態(tài)類型和參數(shù)的靜態(tài)類型這兩個宗量進行選擇,屬于多分派翘县。
Java語言的動態(tài)分派根據(jù)方法接收者的實際類型進行選擇最域,屬于單分派。

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

動態(tài)類型語言的特征:
(1)類型檢查的主體過程在運行期而不是編譯期(如JavaScript)
(2)變量無類型而變量值才有類型(如var)

調(diào)用方法的指令(invokevirtual锈麸、invokespecial镀脂、invokestatic、invokeinterface)的第一個參數(shù)都是被調(diào)用的方法的符號引用(CONSTANT_Methodref_info / CONSTANT_InterfaceMethodref_info)忘伞,方法的符號引用在編譯時產(chǎn)生薄翅,而動態(tài)類型語言只有在運行期才能確定接收者類型,因此這四條調(diào)用方法的指令不能用于實現(xiàn)動態(tài)類型語言氓奈。

為提供動態(tài)類型語言支持翘魄,JDK1.7中引入了invokedynamic指令和java.lang.invoke包。

java.lang.invoke包
這個包的主要目的是在之前單純依靠符號引用來確定目標方法這種方式外舀奶,提供一種新的動態(tài)確定目標方法的機制暑竟,稱為MethodHandle。

利用 java.lang.invoke 包和 java.lang.reflect 包實現(xiàn)相同功能:

package test;

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

public class TestMethodHandle {
    static class A {
        public void print(String a) {
            System.out.println("A -- " + a);
        }
    }

    static class B {
        public void print(String a) {
            System.out.println("B -- " + a);
        }
    }

    /**
     * 根據(jù)方法名伪节、方法類型獲取具體方法
     *
     * @param receiver 方法的調(diào)用者(接收者)
     * @return
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     */
    private static MethodHandle getMethodHandle(Object receiver) throws NoSuchMethodException, IllegalAccessException {
        // 定義方法的類型(返回類型光羞、參數(shù)類型)
        MethodType methodType = MethodType.methodType(void.class, String.class);

        // 調(diào)用lookup方法,在指定類(reveiver對應的類)中查找符合方法名怀大、方法類型纱兑、調(diào)用權(quán)限的方法句柄
        // 將找到的方法綁定到調(diào)用對象上(相當于往方法中加入了表示當前調(diào)用對象的this屬性)
        Lookup lookup = MethodHandles.lookup();
        MethodHandle methodHandle = lookup.findVirtual(receiver.getClass(), "print", methodType).bindTo(receiver);

        return methodHandle;
    }

    public static void main(String[] args) throws Throwable {
        for (int i = 0; i < 5; i++) {
            Object object = i % 2 == 0 ? new A() : new B();
            MethodHandle methodHandle = getMethodHandle(object);
            methodHandle.invoke("123");
        }
    }
}

// 結(jié)果
A -- 123
B -- 123
A -- 123
B -- 123
A -- 123

(1)根據(jù)方法名、方法類型化借,調(diào)用MethodHandles.lookup()函數(shù)去方法中指定的類內(nèi)查找匹配的方法潜慎。
(2)初始化對象實例,然后用對象實例去調(diào)用此方法蓖康。


package test;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class TestReflect {
    static class A {
        public void print(String a) {
            System.out.println("A -- " + a);
        }
    }

    static class B {
        public void print(String a) {
            System.out.println("B -- " + a);
        }
    }

    public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException,
            IllegalArgumentException, InvocationTargetException {
        for (int i = 0; i < 5; i++) {
            Object object = i % 2 == 0 ? new A() : new B();
            Method method = object.getClass().getMethod("print", String.class);
            method.invoke(object, "123");
        }
    }
}

// 結(jié)果
A -- 123
B -- 123
A -- 123
B -- 123
A -- 123

(1)根據(jù)方法名铐炫、參數(shù)類型,利用反射去指定的類內(nèi)查找匹配的方法蒜焊。
(2)初始化對象實例倒信,然后用對象實例去調(diào)用此方法。

Reflection和MethodHandle很像泳梆,但也有區(qū)別:

  • Reflection和MethodHandle都是在模擬方法調(diào)用鳖悠,但是Reflection模擬Java代碼層次的方法調(diào)用榜掌,MethodHandle模擬字節(jié)碼層次的方法調(diào)用。lookup中的三個方法正好對應字節(jié)碼中方法調(diào)用的四條指令:findStatic() - invokestatic乘综,findSpecial() - invokespecial憎账,findVirtual() - invokevirtual和invokeinterfaces
  • java.lang.reflect.Method對象中包含的信息遠多于java.lang.invoke.MethodHandle對象

invokedynamic指令
invokedynamic指令和MethodHandle類似,都是為了把如何查找目標方法的決定權(quán)從虛擬機轉(zhuǎn)移到具體用戶代碼中卡辰。

invokedynamic指令的第一個參數(shù)不再是代表方法符號引用的CONSTANT_Methodref_info常量胞皱,而是CONSTANT_InvokeDynamic_info常量,此常量中包含三個信息:引導方法(Bootstrap Method)九妈、方法類型(MethodType)和名稱反砌。利用引導方法可以得到真正要執(zhí)行的目標方法調(diào)用。

public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType methodType) throws Throwable {
    return new ConstantCallSite(lookup.findStatic(xxx.class, name, methodType));
}

invokedynamic指令是面向Java虛擬機上所有語言的允蚣,因此利用javac無法生成帶有invokedynamic指令的字節(jié)碼于颖,利用INDY工具可以把程序的字節(jié)碼轉(zhuǎn)換為使用invokedynamic指令。

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

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

Java語言中,javac編譯器完成了程序源碼經(jīng)過詞法分析冒晰、語法分析到抽象語法樹同衣,再遍歷語法樹生成線性的字節(jié)碼指令流的過程,這一部分動作由javac完成壶运,獨立在Java虛擬機之外耐齐。
解釋器解釋執(zhí)行指令流的動作在Java虛擬機內(nèi)部完成。

基于棧的指令集和基于寄存器的指令集
javac編譯輸出的字節(jié)碼指令流蒋情,基本上都是基于棧的指令集架構(gòu)埠况,指令集中的指令大部分都是零地址指令,它們依賴操作數(shù)棧進行工作棵癣。

基于棧的指令集和基于寄存器的指令集的對比:
(1)由于寄存器是由硬件直接提供的辕翰,因此如果指令集直接依賴于寄存器,則必然要受到硬件的約束狈谊,但是基于棧的指令集沒有此約束喜命,因此基于棧的指令集可移植性更高。
(2)基于棧的指令集中指令基本都是零地址指令河劝,因此不需要考慮空間分配問題壁榕,直接在棧上操作即可,因此編譯器的實現(xiàn)更簡單赎瞎。
(3)棧的實現(xiàn)是在內(nèi)存中的牌里,寄存器在處理器內(nèi),內(nèi)存的速度遠低于處理器务甥,而且入棧二庵、出棧操作必然會導致指令數(shù)量的增加贪染,因此基于棧的指令集的執(zhí)行速度較慢缓呛。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末催享,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子哟绊,更是在濱河造成了極大的恐慌因妙,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件票髓,死亡現(xiàn)場離奇詭異攀涵,居然都是意外死亡,警方通過查閱死者的電腦和手機洽沟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進店門以故,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人裆操,你說我怎么就攤上這事怒详。” “怎么了踪区?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵昆烁,是天一觀的道長。 經(jīng)常有香客問我缎岗,道長静尼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任传泊,我火速辦了婚禮鼠渺,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘眷细。我一直安慰自己拦盹,他們只是感情好,可當我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布薪鹦。 她就那樣靜靜地躺著掌敬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪池磁。 梳的紋絲不亂的頭發(fā)上奔害,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機與錄音地熄,去河邊找鬼华临。 笑死,一個胖子當著我的面吹牛端考,可吹牛的內(nèi)容都是我干的雅潭。 我是一名探鬼主播揭厚,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼扶供!你這毒婦竟也來了筛圆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤椿浓,失蹤者是張志新(化名)和其女友劉穎太援,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扳碍,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡提岔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了笋敞。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碱蒙。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖夯巷,靈堂內(nèi)的尸體忽然破棺而出赛惩,到底是詐尸還是另有隱情,我是刑警寧澤鞭莽,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布坊秸,位于F島的核電站,受9級特大地震影響澎怒,放射性物質(zhì)發(fā)生泄漏褒搔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一喷面、第九天 我趴在偏房一處隱蔽的房頂上張望星瘾。 院中可真熱鬧,春花似錦惧辈、人聲如沸琳状。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽念逞。三九已至,卻和暖如春边翁,著一層夾襖步出監(jiān)牢的瞬間翎承,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工符匾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叨咖,地道東北人。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像甸各,于是被迫代替她去往敵國和親垛贤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,843評論 2 354

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