在不同的虛擬機實現(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í)行該方法羹应。
只要能被invokestatic
和invokespecial
指令調(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í)行速度較慢缓呛。