本文首發(fā)于微信公眾號(hào):BaronTalk
執(zhí)行引擎是 Java 虛擬機(jī)最核心的組成部分之一?!柑摂M機(jī)」是相對(duì)于「物理機(jī)」的概念,這兩種機(jī)器都有代碼執(zhí)行的能力,區(qū)別是物理機(jī)的執(zhí)行引擎是直接建立在處理器、硬件、指令集和操作系統(tǒng)層面上的,而虛擬機(jī)執(zhí)行引擎是由自己實(shí)現(xiàn)的,因此可以自行制定指令集與執(zhí)行引擎的結(jié)構(gòu)體系,并且能夠執(zhí)行那些不被硬件直接支持的指令集格式。
在 Java 虛擬機(jī)規(guī)范中制定了虛擬機(jī)字節(jié)碼執(zhí)行引擎的概念模型,這個(gè)概念模型成為各種虛擬機(jī)執(zhí)行引擎的統(tǒng)一外觀(guān)(Facade)。在不同的虛擬機(jī)實(shí)現(xiàn)里,執(zhí)行引擎在執(zhí)行 Java 代碼的時(shí)候可能會(huì)有解釋執(zhí)行(通過(guò)解釋器執(zhí)行)和編譯執(zhí)行(通過(guò)即時(shí)編譯器產(chǎn)生本地代碼執(zhí)行)兩種方式,也可能兩者都有,甚至還可能會(huì)包含幾個(gè)不同級(jí)別的編譯器執(zhí)行引擎。但從外觀(guān)上來(lái)看,所有 Java 虛擬機(jī)的執(zhí)行引擎是一致的:輸入的是字節(jié)碼文件,處理過(guò)程是字節(jié)碼解析的等效過(guò)程,輸出的是執(zhí)行結(jié)果。
一. 運(yùn)行時(shí)棧幀結(jié)構(gòu)
棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧的棧元素。棧幀存儲(chǔ)了方法的局部變量、操作數(shù)棧、動(dòng)態(tài)鏈接和方法返回地址等信息。每一個(gè)方法從調(diào)用開(kāi)始到執(zhí)行完成的過(guò)程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里從入棧到出棧的過(guò)程。
每一個(gè)棧幀都包括了局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時(shí),棧幀中需要多大的局部變量表,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫(xiě)入到方法表的 Code 屬性之中,因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。
一個(gè)線(xiàn)程中的方法調(diào)用鏈可能會(huì)很長(zhǎng),很多方法都處于執(zhí)行狀態(tài)。對(duì)于執(zhí)行引擎來(lái)說(shuō),在活動(dòng)線(xiàn)程中,只有位于棧頂?shù)臈攀怯行У?,稱(chēng)為當(dāng)前棧幀(Current Stack Frame),與這個(gè)棧幀相關(guān)聯(lián)的方法成為當(dāng)前方法。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令對(duì)當(dāng)前棧幀進(jìn)行操作,在概念模型上,典型的棧幀結(jié)構(gòu)如下圖:

局部變量表
局部變量表(Local Variable Table)是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在 Java 程序中編譯為 Class 文件時(shí),就在方法的 Code 屬性的 max_locals 數(shù)據(jù)項(xiàng)中確定了該方法所需要分配的局部變量表的最大容量。
操作數(shù)棧
操作數(shù)棧(Operand Stack)是一個(gè)后進(jìn)先出棧。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯階段寫(xiě)入到 Code 屬性的 max_stacks 數(shù)據(jù)項(xiàng)中。操作數(shù)棧的每一個(gè)元素可以是任意的 Java 數(shù)據(jù)類(lèi)型,包括 long 和 double。32 位數(shù)據(jù)類(lèi)型所占的棧容量為 1,64 位數(shù)據(jù)類(lèi)型所占的棧容量為 2。在方法執(zhí)行的任何時(shí)候,操作數(shù)棧的深度都不會(huì)超過(guò) max_stacks 數(shù)據(jù)項(xiàng)中設(shè)定的最大值。
一個(gè)方法剛開(kāi)始執(zhí)行的時(shí)候,該方法的操作數(shù)棧是空的,在方法的執(zhí)行過(guò)程中,會(huì)有各種字節(jié)碼指令往操作數(shù)棧中寫(xiě)入和提取內(nèi)容,也就是入棧和出棧操作。
動(dòng)態(tài)鏈接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)鏈接(Dynamic Linking)。Class 文件的常量池中存在大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用作為參數(shù),這些符號(hào)引用一部分會(huì)在類(lèi)加載階段或第一次使用時(shí)轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化成為靜態(tài)解析。另一部分將在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,這部分稱(chēng)為動(dòng)態(tài)連接。
方法返回地址
當(dāng)一個(gè)方法開(kāi)始執(zhí)行后,只有兩種方式可以退出這個(gè)方法。
一種是執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令,這時(shí)候可能會(huì)有返回值傳遞給上層方法的調(diào)用者,是否有返回值和返回值的類(lèi)型將根據(jù)遇到何種方法返回指令來(lái)決定,這種退出方法的方式稱(chēng)為正常完成出口。
另一種退出方式是,在方法執(zhí)行過(guò)程中遇到了異常,并且這個(gè)異常沒(méi)有在方法體內(nèi)得到處理,無(wú)論是 Java 虛擬機(jī)內(nèi)部產(chǎn)生的異常,還是代碼中使用 athrow 字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒(méi)有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法退出。這種稱(chēng)為異常完成出口。一個(gè)方法使用異常完成出口的方式退出,是不會(huì)給上層調(diào)用者產(chǎn)生任何返回值的。
無(wú)論采用何種退出方式,在方法退出后都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回時(shí)可能需要在棧幀中保存一些信息,用來(lái)恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來(lái)說(shuō),方法正常退出時(shí),調(diào)用者的 PC 計(jì)數(shù)器的值可以作為返回地址,棧幀中很可能會(huì)保存這個(gè)計(jì)數(shù)器值。而方法異常退出時(shí),返回地址是要通過(guò)異常處理器表來(lái)確定的,棧幀中一般不會(huì)保存這部分信息。
方法退出的過(guò)程實(shí)際上就等同于把當(dāng)前棧幀出棧,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上次方法的局部變量表和操作數(shù)棧,把返回值(如果有的話(huà))壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整 PC 計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。
附加信息
虛擬機(jī)規(guī)范允許具體的虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒(méi)有描述的信息到棧幀中,例如與調(diào)試相關(guān)的信息,這部分信息完全取決于具體的虛擬機(jī)實(shí)現(xiàn)。實(shí)際開(kāi)發(fā)中,一般會(huì)把動(dòng)態(tài)連接、方法返回地址與其他附加信息全部歸為一類(lèi),成為棧幀信息。
二. 方法調(diào)用
方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個(gè)方法),暫時(shí)還不涉及方法內(nèi)部的具體運(yùn)行過(guò)程。
在程序運(yùn)行時(shí),進(jìn)行方法調(diào)用是最為普遍、頻繁的操作。前面說(shuō)過(guò) Class 文件的編譯過(guò)程是不包含傳統(tǒng)編譯中的連接步驟的,一切方法調(diào)用在 Class 文件里面存儲(chǔ)的都只是符號(hào)引用,而不是方法在運(yùn)行時(shí)內(nèi)存布局中的入口地址(相當(dāng)于之前說(shuō)的直接引用)。這個(gè)特性給 Java 帶來(lái)了更強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力,但也使得 Java 方法調(diào)用過(guò)程變得相對(duì)復(fù)雜起來(lái),需要在類(lèi)加載期間,甚至到運(yùn)行期間才能確定目標(biāo)方法的直接引用。
解析
所有方法調(diào)用中的目標(biāo)方法在 Class 文件里都是一個(gè)常量池中的符號(hào)引用,在類(lèi)加載的解析階段,會(huì)將其中一部分符號(hào)引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是方法在程序真正運(yùn)行之前就有一個(gè)可確定的調(diào)用版本,并且這個(gè)方法的調(diào)用版本在運(yùn)行期是不可改變的。話(huà)句話(huà)說(shuō),調(diào)用目標(biāo)在程序代碼寫(xiě)好、編譯器進(jìn)行編譯時(shí)就必須確定下來(lái)。這類(lèi)方法的調(diào)用稱(chēng)為解析(Resolution)。
Java 語(yǔ)言中符合「編譯器可知,運(yùn)行期不可變」這個(gè)要求的方法,主要包括靜態(tài)方法和私有方法兩大類(lèi),前者與類(lèi)型直接關(guān)聯(lián),后者在外部不可被訪(fǎng)問(wèn),這兩種方法各自的特點(diǎn)決定了它們都不可能通過(guò)繼承或者別的方式重寫(xiě)其它版本,因此它們都適合在類(lèi)加載階段解析。
與之相應(yīng)的是,在 Java 虛擬機(jī)里提供了 5 條方法調(diào)用字節(jié)碼指令,分別是:
- invokestatic:調(diào)用靜態(tài)方法;
- invokespecial:調(diào)用實(shí)例構(gòu)造器 <init> 方法、私有方法和父類(lèi)方法;
- invokevirtual:調(diào)用所有虛方法;
- invokeinterface:調(diào)用接口方法,會(huì)在運(yùn)行時(shí)再確定一個(gè)實(shí)現(xiàn)此接口的對(duì)象;
- invokedynamic:先在運(yùn)行時(shí)動(dòng)態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法,然后再執(zhí)行該方法。
只要能被 invokestatic 和 invokespecial 指令調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本,符合這個(gè)條件的有靜態(tài)方法、私有方法、實(shí)例構(gòu)造器、父類(lèi)方法 4 類(lèi),它們?cè)诩虞d的時(shí)候就會(huì)把符號(hào)引用解析為直接引用。這些方法可以稱(chēng)為非虛方法,與之相反,其它方法稱(chēng)為虛方法(final 方法除外)。
Java 中的非虛方法除了使用 invokestatic、invokespecial 調(diào)用的方法之外還有一種,就是被 final 修飾的方法。雖然 final 方法是使用 invokevirtual 指令來(lái)調(diào)用的,但是由于它無(wú)法被覆蓋,沒(méi)有其它版本,所以也無(wú)需對(duì)方法接受者進(jìn)行多態(tài)選擇,又或者說(shuō)多態(tài)選擇的結(jié)果肯定是唯一的。在 Java 語(yǔ)言規(guī)范中明確說(shuō)明了 final 方法是一種非虛方法。
解析調(diào)用一定是個(gè)靜態(tài)過(guò)程,在編譯期間就能完全確定,在類(lèi)裝載的解析階段就會(huì)把涉及的符號(hào)引用全部轉(zhuǎn)變?yōu)榭纱_定的直接引用,不會(huì)延遲到運(yùn)行期再去完成。而分派(Dispatch)調(diào)用則可能是靜態(tài)的也可能是動(dòng)態(tài)的,根據(jù)分派依據(jù)的宗量數(shù)可分為單分派和多分派。這兩類(lèi)分派方式的兩兩組合就構(gòu)成了靜態(tài)單分派、靜態(tài)多分派、動(dòng)態(tài)單分派、動(dòng)態(tài)多分派 4 種分派組合情況,下面我們?cè)倏纯刺摂M機(jī)中的方法分派是如何進(jìn)行的。
分派
面向?qū)ο笥腥齻€(gè)基本特征,封裝、繼承和多態(tài)。這里要說(shuō)的分派將會(huì)揭示多態(tài)特征的一些最基本的體現(xiàn),如「重載」和「重寫(xiě)」在 Java 虛擬機(jī)中是如何實(shí)現(xiàn)的?虛擬機(jī)是如何確定正確目標(biāo)方法的?
靜態(tài)分派
在開(kāi)始介紹靜態(tài)分派前我們先看一段代碼。
/**
* 方法靜態(tài)分派演示
*
* @author baronzhang
*/
public class StaticDispatch {
private static abstract class Human { }
private static class Man extends Human { }
private static class Woman extends Human { }
private void sayHello(Human guy) {
System.out.println("Hello, guy!");
}
private void sayHello(Man man) {
System.out.println("Hello, man!");
}
private void sayHello(Woman woman) {
System.out.println("Hello, woman!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch dispatch = new StaticDispatch();
dispatch.sayHello(man);
dispatch.sayHello(woman);
}
}
運(yùn)行后這段程序的輸出結(jié)果如下:
Hello, guy!
Hello, guy!
稍有經(jīng)驗(yàn)的 Java 程序員都能得出上述結(jié)論,但為什么我們傳遞給 sayHello() 方法的實(shí)際參數(shù)類(lèi)型是 Man 和 Woman,虛擬機(jī)在執(zhí)行程序時(shí)選擇的卻是 Human 的重載呢?要理解這個(gè)問(wèn)題,我們先弄清兩個(gè)概念。
Human man = new Man();
上面這段代碼中的「Human」稱(chēng)為變量的靜態(tài)類(lèi)型(Static Type),或者叫做外觀(guān)類(lèi)型(Apparent Type),后面的「Man」稱(chēng)為變量為實(shí)際類(lèi)型(Actual Type),靜態(tài)類(lèi)型和實(shí)際類(lèi)型在程序中都可以發(fā)生一些變化,區(qū)別是靜態(tài)類(lèi)型的變化僅發(fā)生在使用時(shí),變量本身的靜態(tài)類(lèi)型不會(huì)被改變,并且最終的靜態(tài)類(lèi)型是在編譯期可知的;而實(shí)際類(lèi)型變化的結(jié)果在運(yùn)行期才可確定,編譯器在編譯程序的時(shí)候并不知道一個(gè)對(duì)象的實(shí)際類(lèi)型是什么。
弄清了這兩個(gè)概念,再來(lái)看 StaticDispatch 類(lèi)中 main() 方法里的兩次 sayHello() 調(diào)用,在方法接受者已經(jīng)確定是對(duì)象「dispatch」的前提下,使用哪個(gè)重載版本,就完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類(lèi)型。代碼中定義了兩個(gè)靜態(tài)類(lèi)型相同但是實(shí)際類(lèi)型不同的變量,但是虛擬機(jī)(準(zhǔn)確的說(shuō)是編譯器)在重載時(shí)是通過(guò)參數(shù)的靜態(tài)類(lèi)型而不是實(shí)際類(lèi)型作為判定依據(jù)的。并且靜態(tài)類(lèi)型是編譯期可知的,因此在編譯階段, Javac 編譯器會(huì)根據(jù)參數(shù)的靜態(tài)類(lèi)型決定使用哪個(gè)重載版本,所以選擇了 sayHello(Human) 作為調(diào)用目標(biāo),并把這個(gè)方法的符號(hào)引用寫(xiě)到 man() 方法里的兩條 invokevirtual 指令的參數(shù)中。
所有依賴(lài)靜態(tài)類(lèi)型來(lái)定位方法執(zhí)行版本的分派動(dòng)作稱(chēng)為靜態(tài)分派。靜態(tài)分派的典型應(yīng)用是方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動(dòng)作實(shí)際上不是由虛擬機(jī)來(lái)執(zhí)行的。
另外,編譯器雖然能確定方法的重載版本,但是很多情況下這個(gè)重載版本并不是「唯一」的,因此往往只能確定一個(gè)「更加合適」的版本。產(chǎn)生這種情況的主要原因是字面量不需要定義,所以字面量沒(méi)有顯示的靜態(tài)類(lèi)型,它的靜態(tài)類(lèi)型只能通過(guò)語(yǔ)言上的規(guī)則去理解和推斷。下面的代碼展示了什么叫「更加合適」的版本。
/**
* @author baronzhang
*/
public class Overlaod {
static void sayHello(Object arg) {
System.out.println("Hello, Object!");
}
static void sayHello(int arg) {
System.out.println("Hello, int!");
}
static void sayHello(long arg) {
System.out.println("Hello, long!");
}
static void sayHello(Character arg) {
System.out.println("Hello, Character!");
}
static void sayHello(char arg) {
System.out.println("Hello, char!");
}
static void sayHello(char... arg) {
System.out.println("Hello, char...!");
}
static void sayHello(Serializable arg) {
System.out.println("Hello, Serializable!");
}
public static void main(String[] args) {
sayHello('a');
}
}
上面代碼的運(yùn)行結(jié)果為:
Hello, char!
這很好理解,‘a(chǎn)’ 是一個(gè) char 類(lèi)型的數(shù)據(jù),自然會(huì)尋找參數(shù)類(lèi)型為 char 的重載方法,如果注釋掉 sayHello(chat arg) 方法,那么輸出結(jié)果將會(huì)變?yōu)椋?/p>
Hello, int!
這時(shí)發(fā)生了一次類(lèi)型轉(zhuǎn)換, ‘a(chǎn)’ 除了可以代表一個(gè)字符,還可以代表數(shù)字 97,因?yàn)樽址?‘a(chǎn)’ 的 Unicode 數(shù)值為十進(jìn)制數(shù)字 97,因此參數(shù)類(lèi)型為 int 的重載方法也是合適的。我們繼續(xù)注釋掉 sayHello(int arg) 方法,輸出變?yōu)椋?/p>
Hello, long!
這時(shí)發(fā)生了兩次類(lèi)型轉(zhuǎn)換,‘a(chǎn)’ 轉(zhuǎn)型為整數(shù) 97 之后,進(jìn)一步轉(zhuǎn)型為長(zhǎng)整型 97L,匹配了參數(shù)類(lèi)型為 long 的重載方法。我們繼續(xù)注釋掉 sayHello(long arg) 方法,輸出變?yōu)椋?/p>
Hello, Character!
這時(shí)發(fā)生了一次自動(dòng)裝箱, ‘a(chǎn)’ 被包裝為它的封裝類(lèi)型 java.lang.Character,所以匹配到了類(lèi)型為 Character 的重載方法,繼續(xù)注釋掉 sayHello(Character arg) 方法,輸出變?yōu)椋?/p>
Hello, Serializable!
這里輸出之所以為「Hello, Serializable!」,是因?yàn)?java.lang.Serializable 是 java.lang.Character 類(lèi)實(shí)現(xiàn)的一個(gè)接口,當(dāng)自動(dòng)裝箱后發(fā)現(xiàn)還是找不到裝箱類(lèi),但是找到了裝箱類(lèi)實(shí)現(xiàn)了的接口類(lèi)型,所以緊接著又發(fā)生了一次自動(dòng)轉(zhuǎn)換。char 可以轉(zhuǎn)型為 int,但是 Character 是絕對(duì)不會(huì)轉(zhuǎn)型為 Integer 的,他只能安全的轉(zhuǎn)型為它實(shí)現(xiàn)的接口或父類(lèi)。Character 還實(shí)現(xiàn)了另外一個(gè)接口 java.lang.Comparable<Character>,如果同時(shí)出現(xiàn)兩個(gè)參數(shù)分別為 Serializable 和 Comparable<Character> 的重載方法,那它們?cè)诖藭r(shí)的優(yōu)先級(jí)是一樣的。編譯器無(wú)法確定要自動(dòng)轉(zhuǎn)型為哪種類(lèi)型,會(huì)提示類(lèi)型模糊,拒絕編譯。程序必須在調(diào)用時(shí)顯示的指定字面量的靜態(tài)類(lèi)型,如:sayHello((Comparable<Character>) 'a'),才能編譯通過(guò)。繼續(xù)注釋掉 sayHello(Serializable arg) 方法,輸出變?yōu)椋?/p>
Hello, Object!
這時(shí)是 char 裝箱后轉(zhuǎn)型為父類(lèi)了,如果有多個(gè)父類(lèi),那將在繼承關(guān)系中從下往上開(kāi)始搜索,越接近上層的優(yōu)先級(jí)越低。即使方法調(diào)用的入?yún)⒅禐?null,這個(gè)規(guī)則依然適用。繼續(xù)注釋掉 sayHello(Serializable arg) 方法,輸出變?yōu)椋?/p>
Hello, char...!
7 個(gè)重載方法以及被注釋得只剩一個(gè)了,可見(jiàn)變長(zhǎng)參數(shù)的重載優(yōu)先級(jí)是最低的,這時(shí)字符 ‘a(chǎn)’ 被當(dāng)成了一個(gè)數(shù)組元素。
前面介紹的這一系列過(guò)程演示了編譯期間選擇靜態(tài)分派目標(biāo)的過(guò)程,這個(gè)過(guò)程也是 Java 語(yǔ)言實(shí)現(xiàn)方法重載的本質(zhì)。
動(dòng)態(tài)分派
動(dòng)態(tài)分派和多態(tài)性的另一個(gè)重要體現(xiàn)「重寫(xiě)(Override)」有著密切的關(guān)聯(lián),我們依舊通過(guò)代碼來(lái)理解什么是動(dòng)態(tài)分派。
/**
* 方法動(dòng)態(tài)分派演示
*
* @author baronzhang
*/
public class DynamicDispatch {
static abstract class Human {
abstract void sayHello();
}
static class Man extends Human {
@Override
void sayHello() {
System.out.println("Man say hello!");
}
}
static class Woman extends Human {
@Override
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();
}
}
代碼執(zhí)行結(jié)果:
Man say hello!
Woman say hello!
Woman say hello!
對(duì)于上面的代碼,虛擬機(jī)是如何確定要調(diào)用哪個(gè)方法的呢?顯然這里不再通過(guò)靜態(tài)類(lèi)型來(lái)決定了,因?yàn)殪o態(tài)類(lèi)型同樣都是 Human 的兩個(gè)變量 man 和 woman 在調(diào)用 sayHello() 方法時(shí)執(zhí)行了不同的行為,并且變量 man 在兩次調(diào)用中執(zhí)行了不同的方法。導(dǎo)致這個(gè)結(jié)果的原因是因?yàn)樗鼈兊膶?shí)際類(lèi)型不同。對(duì)于虛擬機(jī)是如何通過(guò)實(shí)際類(lèi)型來(lái)分派方法執(zhí)行版本的,這里我們就不做介紹了,有興趣的可以去看看原著。
我們把這種在運(yùn)行期根據(jù)實(shí)際類(lèi)型來(lái)確定方法執(zhí)行版本的分派稱(chēng)為動(dòng)態(tài)分派。
單分派和多分派
方法的接收者和方法的參數(shù)統(tǒng)稱(chēng)為方法的宗量,這個(gè)定義最早來(lái)源于《Java 與模式》一書(shū)。根據(jù)分派基于多少宗量,可將分派劃分為單分派和多分派。
單分派是根據(jù)一個(gè)宗量來(lái)確定方法的執(zhí)行版本;多分派則是根據(jù)多余一個(gè)宗量來(lái)確定方法的執(zhí)行版本。
我們依舊通過(guò)代碼來(lái)理解(代碼以著名的 3Q 大戰(zhàn)作為背景):
/**
* 單分派、多分派演示
*
* @author baronzhang
*/
public class Dispatch {
static class QQ { }
static class QiHu360 { }
static class Father {
public void hardChoice(QQ qq) {
System.out.println("Father choice QQ!");
}
public void hardChoice(QiHu360 qiHu360) {
System.out.println("Father choice 360!");
}
}
static class Son extends Father {
@Override
public void hardChoice(QQ qq) {
System.out.println("Son choice QQ!");
}
@Override
public void hardChoice(QiHu360 qiHu360) {
System.out.println("Son choice 360!");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new QQ());
son.hardChoice(new QiHu360());
}
}
代碼輸出結(jié)果:
Father choice QQ!
Son choice 360!
我們先來(lái)看看編譯階段編譯器的選擇過(guò)程,也就是靜態(tài)分派過(guò)程。這個(gè)時(shí)候選擇目標(biāo)方法的依據(jù)有兩點(diǎn):一是靜態(tài)類(lèi)型是 Father 還是 Son;二是方法入?yún)⑹?QQ 還是 QiHu360。因?yàn)槭歉鶕?jù)兩個(gè)宗量進(jìn)行選擇的,所以 Java 語(yǔ)言的靜態(tài)分派屬于多分派。
再看看運(yùn)行階段虛擬機(jī)的選擇過(guò)程,也就是動(dòng)態(tài)分派的過(guò)程。在執(zhí)行 son.hardChoice(new QiHu360()) 時(shí),由于編譯期已經(jīng)確定目標(biāo)方法的簽名必須為 hardChoice(QiHu360),這時(shí)參數(shù)的靜態(tài)類(lèi)型、實(shí)際類(lèi)型都不會(huì)對(duì)方法的選擇造成任何影響,唯一可以影響虛擬機(jī)選擇的因數(shù)只有此方法的接收者的實(shí)際類(lèi)型是 Father 還是 Son。因?yàn)橹挥幸粋€(gè)宗量作為選擇依據(jù),所以 Java 語(yǔ)言的動(dòng)態(tài)分派屬于單分派。
綜上所述,Java 語(yǔ)言是一門(mén)靜態(tài)多分派、動(dòng)態(tài)單分派的語(yǔ)言。
三. 基于棧的字節(jié)碼解釋執(zhí)行引擎
虛擬機(jī)如何調(diào)用方法已經(jīng)介紹完了,下面我們來(lái)看看虛擬機(jī)是如何執(zhí)行方法中的字節(jié)碼指令的。
解釋執(zhí)行
Java 語(yǔ)言常被人們定義成「解釋執(zhí)行」的語(yǔ)言,但隨著 JIT 以及可直接將 Java 代碼編譯成本地代碼的編譯器的出現(xiàn),這種說(shuō)法就不對(duì)了。只有確定了談?wù)搶?duì)象是某種具體的 Java 實(shí)現(xiàn)版本和執(zhí)行引擎運(yùn)行模式時(shí),談解釋執(zhí)行還是編譯執(zhí)行才會(huì)比較確切。
無(wú)論是解釋執(zhí)行還是編譯執(zhí)行,無(wú)論是物理機(jī)還是虛擬機(jī),對(duì)于應(yīng)用程序,機(jī)器都不可能像人一樣閱讀、理解,然后獲得執(zhí)行能力。大部分的程序代碼到物理機(jī)的目標(biāo)代碼或者虛擬機(jī)執(zhí)行的指令之前,都需要經(jīng)過(guò)下圖中的各個(gè)步驟。下圖中最下面的那條分支,就是傳統(tǒng)編譯原理中程序代碼到目標(biāo)機(jī)器代碼的生成過(guò)程;中間那條分支,則是解釋執(zhí)行的過(guò)程。

如今,基于物理機(jī)、Java 虛擬機(jī)或者非 Java 的其它高級(jí)語(yǔ)言虛擬機(jī)的語(yǔ)言,大多都會(huì)遵循這種基于現(xiàn)代編譯原理的思路,在執(zhí)行前先對(duì)程序源代碼進(jìn)行詞法分析和語(yǔ)法分析處理,把源代碼轉(zhuǎn)化為抽象語(yǔ)法樹(shù)。對(duì)于一門(mén)具體語(yǔ)言的實(shí)現(xiàn)來(lái)說(shuō),詞法分析、語(yǔ)法分析以至后面的優(yōu)化器和目標(biāo)代碼生成器都可以選擇獨(dú)立于執(zhí)行引擎,形成一個(gè)完整意義的編譯器去實(shí)現(xiàn),這類(lèi)代表是 C/C++。也可以為一個(gè)半獨(dú)立的編譯器,這類(lèi)代表是 Java。又或者把這些步驟和執(zhí)行全部封裝在一個(gè)封閉的黑匣子中,如大多數(shù)的 JavaScript 執(zhí)行器。
Java 語(yǔ)言中,Javac 編譯器完成了程序代碼經(jīng)過(guò)詞法分析、語(yǔ)法分析到抽象語(yǔ)法樹(shù)、再遍歷語(yǔ)法樹(shù)生成字節(jié)碼指令流的過(guò)程。因?yàn)檫@一部分動(dòng)作是在 Java 虛擬機(jī)之外進(jìn)行的,而解釋器在虛擬機(jī)的內(nèi)部,所以 Java 程序的編譯就是半獨(dú)立的實(shí)現(xiàn)。
許多 Java 虛擬機(jī)的執(zhí)行引擎在執(zhí)行 Java 代碼的時(shí)候都有解釋執(zhí)行(通過(guò)解釋器執(zhí)行)和編譯執(zhí)行(通過(guò)即時(shí)編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇。而對(duì)于最新的 Android 版本的執(zhí)行模式則是 AOT + JIT + 解釋執(zhí)行,關(guān)于這方面我們后面有機(jī)會(huì)再聊。
基于棧的指令集與基于寄存器的指令集
Java 編譯器輸出的指令流,基本上是一種基于棧的指令集架構(gòu)?;跅5闹噶罴饕膬?yōu)點(diǎn)就是可移植,寄存器由硬件直接提供,程序直接依賴(lài)這些硬件寄存器則不可避免的要受到硬件約束。棧架構(gòu)的指令集還有一些其他優(yōu)點(diǎn),比如相對(duì)更加緊湊(字節(jié)碼中每個(gè)字節(jié)就對(duì)應(yīng)一條指令,而多地址指令集中還需要存放參數(shù))、編譯實(shí)現(xiàn)更加簡(jiǎn)單(不需要考慮空間分配的問(wèn)題,所有空間都是在棧上操作)等。
棧架構(gòu)指令集的主要缺點(diǎn)是執(zhí)行速度相對(duì)來(lái)說(shuō)會(huì)稍慢一些。所有主流物理機(jī)的指令集都是寄存器架構(gòu)也從側(cè)面印證了這一點(diǎn)。
雖然棧架構(gòu)指令集的代碼非常緊湊,但是完成相同功能需要的指令集數(shù)量一般會(huì)比寄存器架構(gòu)多,因?yàn)槌鰲?、入棧操作本身就產(chǎn)生了相當(dāng)多的指令數(shù)量。更重要的是,棧實(shí)現(xiàn)在內(nèi)存中,頻繁的棧訪(fǎng)問(wèn)也意味著頻繁的內(nèi)存訪(fǎng)問(wèn),相對(duì)于處理器來(lái)說(shuō),內(nèi)存始終是執(zhí)行速度的瓶頸。由于指令數(shù)量和內(nèi)存訪(fǎng)問(wèn)的原因,所以導(dǎo)致了棧架構(gòu)指令集的執(zhí)行速度會(huì)相對(duì)較慢。
正是基于上述原因,Android 虛擬機(jī)中采用了基于寄存器的指令集架構(gòu)。不過(guò)有一點(diǎn)不同的是,前面說(shuō)的是物理機(jī)上的寄存器,而 Android 上指的是虛擬機(jī)上的寄存器。
寫(xiě)在最后
這一篇我們介紹了虛擬機(jī)是如何執(zhí)行方法中的字節(jié)碼指令的,下一篇文章我們來(lái)重點(diǎn)介紹下虛擬機(jī)是如何優(yōu)化我們所編寫(xiě)的代碼的。
參考資料:
- 《深入理解 Java 虛擬機(jī):JVM 高級(jí)特性與最佳實(shí)踐(第 2 版)》
如果你喜歡我的文章,就關(guān)注下我的公眾號(hào) BaronTalk 、 知乎專(zhuān)欄 或者在 GitHub 上添個(gè) Star 吧!
- 微信公眾號(hào):BaronTalk
- 知乎專(zhuān)欄:https://zhuanlan.zhihu.com/baron
- GitHub:https://github.com/BaronZ88