字節(jié)碼執(zhí)行引擎是Java虛擬機最核心的組成部分之一舔哪。虛擬機是相對于物理機的概念酒繁,兩者都有代碼執(zhí)行能力皮钠。不同的是物理機的執(zhí)行引擎直接建立在物理硬件和操作系統(tǒng)層面上识樱,而虛擬機的執(zhí)行引擎則有自己的指令集艳汽,可以執(zhí)行不被硬件直接支持的指令Java虛擬機規(guī)范中制定了虛擬機執(zhí)行引擎的概念模型猴贰,不同的虛擬機只要滿足這個概念模型的要求(輸入字節(jié)碼,處理過程是字節(jié)碼解析的等效過程河狐,輸出執(zhí)行結(jié)果)米绕,具體的實現(xiàn)可以自行制定(解釋執(zhí)行、編譯執(zhí)行或者兩者結(jié)合等)
- 物理機的執(zhí)行引擎:直接建立在處理器馋艺、硬件栅干、指令集和操作系統(tǒng)層面
- 虛擬機的執(zhí)行引擎:由自己實現(xiàn),可以自行制定指令集與執(zhí)行引擎的結(jié)構(gòu)體系丈钙,并且能夠執(zhí)行不被硬件直接支持的指令集格式非驮。
- java虛擬機的執(zhí)行引擎:輸入字節(jié)碼文件,處理過程是字節(jié)碼解析的等效過程雏赦,輸出是執(zhí)行結(jié)果劫笙。
一、運行時幀棧結(jié)構(gòu)
棧幀是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)星岗,它是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧中的棧元素填大。
存儲內(nèi)容:方法的局部變量表、操作數(shù)棧俏橘、動態(tài)連接和方法返回地址等信息允华。每一個方法從調(diào)用開始至執(zhí)行完成的過程,都對應(yīng)著一個棧幀的入棧和出棧寥掐。
在編譯程序代碼的時候靴寂,棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了召耘,并且寫入到code屬性之中百炬,因此一個棧幀需要分配多少內(nèi)存,不會受程序運行期變量數(shù)據(jù)的影響污它,而僅僅取決于具體的虛擬機實現(xiàn)剖踊。
一個線程中的方法調(diào)用鏈可能會很長庶弃,很多方法都同時處于執(zhí)行狀態(tài)。但是只有位于棧頂?shù)臈攀怯行У牡鲁海Q為“當(dāng)前棧幀”歇攻,與這個棧幀相關(guān)聯(lián)的方法稱為“當(dāng)前方法”。執(zhí)行引擎運行的所有字節(jié)碼指令只對當(dāng)前棧幀進行操作梆造。典型的棧幀結(jié)構(gòu)如下:
1缴守、局部變量表
局部變量表(local variable table) 是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量澳窑。
在java程序編譯為Class文件時斧散,在方法的Code屬性的max_locals數(shù)據(jù)項中確定了該方法所需分配的局部變量表的最大容量。
局部變量表的容量以變量槽(Variable Slot摊聋,簡稱Slot)為最小單位,每個變量槽都能存放一個32位以內(nèi)的數(shù)據(jù)類型由
boolean栈暇、byte麻裁、char、short源祈、int煎源、float、reference香缺、returnAddress類型數(shù)據(jù)手销;64位的數(shù)據(jù)類型虛擬機以高位對齊的方式
為其分配兩個連續(xù)的Slot空間,如double图张、long锋拖。
局部變量表通過索引定位的方式使用局部變量表,索引范圍是0~局部變量表最大Slot數(shù)量祸轮;
第0位用于傳遞方法所屬對象實例的引用兽埃,可以通過關(guān)鍵字“this”來訪問到這個隱含參數(shù)。
類變量有兩次賦初始值的過程适袜,一次在準備階段柄错,賦予系統(tǒng)初始值秉剑;一次在初始化階段赊时,賦予程序員定于的初始值镊辕。
局部變量定義后沒有賦值是不能使用的着倾。
2慎璧、操作數(shù)棧
操作數(shù)棧(operand stack ,操作棧)圣絮,后入先出(last in first out)的棧,最大深度在編譯時寫入Code屬性的max_stacks數(shù)據(jù)項中似炎。
操作棧的每一個元素可以是任意的java數(shù)據(jù)類型槽袄,包括long给僵、double毫捣,32為數(shù)據(jù)類型占棧容量1详拙,64位占2;方法執(zhí)行時蔓同,操作數(shù)棧的
深度不會超過在max_stacks數(shù)據(jù)項中設(shè)定的最大值饶辙。
當(dāng)一個方法剛剛開始執(zhí)行時,方法的操作數(shù)棧為空斑粱,在方法執(zhí)行過程中弃揽,字節(jié)碼指令向操作數(shù)棧寫入和提取內(nèi)容,也就是出棧则北、入棧操作矿微。
3、動態(tài)鏈接
每個棧幀都包含一個執(zhí)行運行時常量池中該棧幀所屬方法的引用尚揣,這個引用為了支持調(diào)用過程中動態(tài)鏈接(Dynamic linking)
4涌矢、方法返回地址
退出方法:
正常完成出口:執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令
異常完成出口:在執(zhí)行方法過程中遇到了異常,并且這個異常沒有在方法體內(nèi)處理快骗。
二娜庇、方法調(diào)用
方法調(diào)用不等同與方法執(zhí)行,方法調(diào)用階段確定調(diào)用哪一個方法方篮,不涉及方法內(nèi)部具體運行過程名秀。
1、解析
所有方法調(diào)用中的模板方法在Class文件里都是一個常量池中的符號引用藕溅,在類加載的解析階段匕得,會將其中一部分的符號引用轉(zhuǎn)化為直接引用,這種解析的前提是在真正運行之前有一個確定的調(diào)用方法巾表,并且該方法在運行期是不可變的汁掠。換句話說:調(diào)用目標在程序代碼寫好、
編譯器進行編譯時就必須確定下來攒发,這類方法的調(diào)用稱為解析(resolution)调塌。Java語言中符合“編譯期可知,運行期不可變”這個要求的方法惠猿,主要包括靜態(tài)方法和私有方法羔砾;靜態(tài)方法與類型直接關(guān)聯(lián),私有方法在外部不可訪問偶妖,這兩種方法各自特點決定了他們都不可能通過繼承和重寫來確定其他版本姜凄,因此都在類加載階段進行解析。
方法調(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í)行該方法申鱼,在此之前的4條調(diào)用指令愤诱,分派邏輯是固化在java虛擬機內(nèi)部的,而invokedynamic指令的分派邏輯是由用戶所設(shè)定的引導(dǎo)方法決定的捐友。
- 只要能被invokestatic淫半、invokespecial指令調(diào)用的方法,都可以在解析階段確定唯一的調(diào)用版本匣砖,
包括靜態(tài)方法科吭、私有方法、實例構(gòu)造器猴鲫、父類方法对人,在類加載時,會把所有的符號引用解析為該方法的直接調(diào)用拂共。這些方法稱為非虛方法牺弄。
非虛方法還包括:final方法,雖然被invokevirtual方法調(diào)用匣缘,猖闪,但是其無法被覆蓋,沒有其他版本肌厨,也就沒有多態(tài)選擇或者說多態(tài)選擇唯一。
解析調(diào)用是一個靜態(tài)調(diào)用的過程豁陆,在編譯期間就完全確定柑爸,在類裝載的解析階段就會把涉及的符號引用替換為可確定的直接引用,不會延遲到運行期完成盒音。
2表鳍、分派
分派調(diào)用可能是靜態(tài)的也可能是動態(tài)的,根據(jù)分派依據(jù)的宗量數(shù)可分為單分派和多分派祥诽,兩類組合就構(gòu)成了靜態(tài)單分派譬圣、靜態(tài)多分派、動態(tài)單分派雄坪、動態(tài)多分派厘熟。
- 靜態(tài)分派
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
public void sayHello(Human human) {
System.out.println("hello guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public void sayHello(Woman woman) {
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é)果:
hello guy!
hello guy!
“Human”稱為靜態(tài)類型(static type),或者叫做外觀類型(apparent Type);后面的“Man”則稱為變量的實際類型(Actual Type)。
靜態(tài)類型是在編譯期可知的维哈,實際類型變化的結(jié)果在運行期才可確定绳姨,編譯器在編譯程序的時候并不知道一個對象的實際類型是什么,使用哪個重載版本阔挠,完全取決于入?yún)⒌臄?shù)量和數(shù)據(jù)類型飘庄,虛擬機(準確的說是編譯器)在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型
作為判斷依據(jù)的,并且靜態(tài)類型是編譯期可知的购撼,因此跪削,在編譯階段谴仙,Javac編譯器會根據(jù)參數(shù)的靜態(tài)類型決定使用哪個重載版本。
所依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作稱為靜態(tài)分派碾盐。靜態(tài)分派典型的應(yīng)用方法是重載晃跺。
靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動作實際上不是由虛擬機來執(zhí)行的廓旬,
- 動態(tài)分派
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();
}
輸出結(jié)果:
man say hello
woman say hello
woman say hello
invokevirtual指令運行時解析的過程:
1哼审、找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記作C孕豹。
2涩盾、如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權(quán)限校驗励背,如果通過則返回這個方法的直接引用春霍,查找過程結(jié)束。
如果不通過叶眉,返回java.lang.IllegalAccessError異常址儒。
3、否則衅疙,按照繼承關(guān)系從下往上一次對C的各個父類進行第2步的搜索和驗證過程莲趣。
4、如果始終沒有找到合適的方法饱溢,則拋出java.lang.AbstactMethodError異常喧伞。
由于invokevirtual指令執(zhí)行的第一步就是在運行期確定接收者的實際類型,所以兩次調(diào)用中的invokevirtual指令把常量池中的類方法符號引
用解析到了不同的直接引用上绩郎,這個過程就是java語言重新的本質(zhì)潘鲫,我們把在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派。
- 單分派與多分派
靜態(tài)分派屬于多分派類型肋杖,因為首先需確實靜態(tài)類型溉仑,然后確定方法參數(shù)
動態(tài)分派屬于單分派類型,因為在執(zhí)行invokevirtual指令時状植,已經(jīng)確定所執(zhí)行的方法浊竟,而可以影響虛擬機選擇的因素只有實際類型。 - 虛擬機動態(tài)分派的實現(xiàn)
通過虛方法表存放各個方法的實際入口地址浅萧,如果某個方法在子類中沒有被重寫逐沙,那子類的虛方法表里面的地址入口和父類相同方法
的地址入口是一致的,都指向父類的實現(xiàn)入口洼畅;如果子類中重寫了這個方法吩案,子類方法表中的地址將會誒替換為指向子類實現(xiàn)版本的入口地址。
java 1.7 是一個動態(tài)單分派帝簇、靜態(tài)多分派的語言
三徘郭、基于棧的字節(jié)碼解釋執(zhí)行引擎
上面已經(jīng)把java虛擬機是如何調(diào)用方法講完了靠益,那么接下來就是虛擬機是怎么執(zhí)行這些字節(jié)碼指令的.虛擬機在執(zhí)行代碼時都有解釋執(zhí)行和編譯執(zhí)行兩種選擇。
解釋執(zhí)行
java語言剛開始的時候被人們定義為解釋執(zhí)行的語言残揉,在jdk1.0來說是比較準確的胧后,但隨著虛擬機的發(fā)展,虛擬機中開始包含了即時編譯器后抱环,class文件中的代碼到底是解釋執(zhí)行還是編譯執(zhí)行恐怕只有虛擬機自己才能判斷了壳快。
不過不管是解釋還是編譯,不管是物理機還是虛擬機镇草,對于應(yīng)用程序眶痰,機器肯定是無法像人一樣閱讀和理解,然后獲得執(zhí)行能力梯啤。大部分的程序代碼到物理機或者虛擬機可執(zhí)行的字節(jié)碼指令集竖伯,都需要經(jīng)歷多個步驟,如下圖因宇,而中間那條就是解釋執(zhí)行的過程七婴。
Java語言中,Javac編譯器完成了程序代碼經(jīng)過詞法分析察滑、語法分析到抽象語法樹打厘,再遍歷樹生成線性的字節(jié)碼指令流的過程。因為一部分在虛擬機外贺辰,而解釋器在虛擬機的內(nèi)部婚惫,所以java程序的編譯就是半獨立的實現(xiàn)。
基于棧的指令集與基于寄存器的指令集
Java編譯器輸出的指令流魂爪,基本上是一種基于棧的指令集架構(gòu),指令流里面大部分都是零地址指令看艰管,他們依賴操作數(shù)棧進行工作滓侍。與之相對的另外一套常用指令集架構(gòu)是基于寄存器的指令集。
兩者優(yōu)缺點:
1.基于棧的指令集主要優(yōu)點就是可移植性牲芋,但因為相同的動作該指令集需要頻繁操作內(nèi)存撩笆,且多于寄存器指令集,速度就慢缸浦。
2.基于寄存器指令集主要優(yōu)點就是速度快夕冲,操作少。但是因為寄存器是依賴于硬件的裂逐,所以它的移植性受到影響歹鱼。
基于棧的解釋器執(zhí)行過程
這一內(nèi)容通過一個一個四則運算進行講解,下面是代碼:
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a+b) * c;
}
下面是字節(jié)碼執(zhí)行過程圖(包含字節(jié)碼指令卜高、pc計數(shù)器弥姻、操作數(shù)棧南片、局部變量表):
上面的演示,是一個概念模型庭敦,實際上肯定不會跟這個一樣的疼进,因為虛擬機中的解釋器和即時編譯器都會對輸入的字節(jié)碼進行優(yōu)化。
本章內(nèi)容思維導(dǎo)圖:
參考:
https://blog.51cto.com/4837471/2159773