每日一言: 朝著一定目標走去是“志”,一鼓作氣中途絕不停止是“氣”萨咳,兩者合起來就是“志氣”懊缺。一切事業(yè)的成敗都取決于此。
執(zhí)行引擎是 Java 虛擬機最核心的組成部分之一培他【榱剑「虛擬機」是相對于「物理機」的概念遗座,這兩種機器都有代碼執(zhí)行的能力,區(qū)別是物理機的執(zhí)行引擎是直接建立在處理器俊扳、硬件员萍、指令集和操作系統(tǒng)層面上的,而虛擬機執(zhí)行引擎是由自己實現(xiàn)的拣度,因此可以自行制定指令集與執(zhí)行引擎的結(jié)構(gòu)體系碎绎,并且能夠執(zhí)行那些不被硬件直接支持的指令集格式。
在 Java 虛擬機規(guī)范中制定了虛擬機字節(jié)碼執(zhí)行引擎的概念模型抗果,這個概念模型成為各種虛擬機執(zhí)行引擎的統(tǒng)一外觀(Facade)筋帖。在不同的虛擬機實現(xiàn)里,執(zhí)行引擎在執(zhí)行 Java 代碼的時候可能會有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時編譯器產(chǎn)生本地代碼執(zhí)行)兩種方式冤馏,也可能兩者都有日麸,甚至還可能會包含幾個不同級別的編譯器執(zhí)行引擎。但從外觀上來看逮光,所有 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)鏈接和方法返回地址等信息。每一個方法從調(diào)用開始到執(zhí)行完成的過程驾茴,都對應(yīng)著一個棧幀在虛擬機棧里從入棧到出棧的過程盼樟。
每一個棧幀都包括了局部變量表、操作數(shù)棧锈至、動態(tài)鏈接晨缴、方法返回地址和一些額外的附加信息。在編譯程序代碼時峡捡,棧幀中需要多大的局部變量表击碗,多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到方法表的 Code 屬性之中棋返,因此一個棧幀需要分配多少內(nèi)存延都,不會受到程序運行期變量數(shù)據(jù)的影響雷猪,而僅僅取決于具體的虛擬機實現(xiàn)睛竣。
一個線程中的方法調(diào)用鏈可能會很長,很多方法都處于執(zhí)行狀態(tài)求摇。對于執(zhí)行引擎來說射沟,在活動線程中殊者,只有位于棧頂?shù)臈攀怯行У模Q為當前棧幀(Current Stack Frame)验夯,與這個棧幀相關(guān)聯(lián)的方法成為當前方法猖吴。執(zhí)行引擎運行的所有字節(jié)碼指令對當前棧幀進行操作,在概念模型上挥转,典型的棧幀結(jié)構(gòu)如下圖:
局部變量表
局部變量表(Local Variable Table)是一組變量值存儲空間海蔽,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在 Java 程序中編譯為 Class 文件時绑谣,就在方法的 Code 屬性的 max_locals 數(shù)據(jù)項中確定了該方法所需要分配的局部變量表的最大容量党窜。
操作數(shù)棧
操作數(shù)棧(Operand Stack)是一個后進先出棧。同局部變量表一樣借宵,操作數(shù)棧的最大深度也在編譯階段寫入到 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è)定的最大值楚里。
一個方法剛開始執(zhí)行的時候,該方法的操作數(shù)棧是空的猎贴,在方法的執(zhí)行過程中腻豌,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是入棧和出棧操作嘱能。
動態(tài)鏈接
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用吝梅,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)鏈接(Dynamic Linking)。Class 文件的常量池中存在大量的符號引用惹骂,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用作為參數(shù)苏携,這些符號引用一部分會在類加載階段或第一次使用時轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化成為靜態(tài)解析对粪。另一部分將在每一次運行期間轉(zhuǎn)化為直接引用右冻,這部分稱為動態(tài)連接。
方法返回地址
當一個方法開始執(zhí)行后著拭,只有兩種方式可以退出這個方法纱扭。
一種是執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令,這時候可能會有返回值傳遞給上層方法的調(diào)用者儡遮,是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定乳蛾,這種退出方法的方式稱為正常完成出口。
另一種退出方式是,在方法執(zhí)行過程中遇到了異常肃叶,并且這個異常沒有在方法體內(nèi)得到處理蹂随,無論是 Java 虛擬機內(nèi)部產(chǎn)生的異常,還是代碼中使用 athrow 字節(jié)碼指令產(chǎn)生的異常因惭,只要在本方法的異常表中沒有搜索到匹配的異常處理器岳锁,就會導(dǎo)致方法退出。這種稱為異常完成出口蹦魔。一個方法使用異常完成出口的方式退出激率,是不會給上層調(diào)用者產(chǎn)生任何返回值的。
無論采用何種退出方式勿决,在方法退出后都需要返回到方法被調(diào)用的位置柱搜,程序才能繼續(xù)執(zhí)行,方法返回時可能需要在棧幀中保存一些信息剥险,用來恢復(fù)它的上層方法的執(zhí)行狀態(tài)聪蘸。一般來說,方法正常退出時表制,調(diào)用者的 PC 計數(shù)器的值可以作為返回地址健爬,棧幀中很可能會保存這個計數(shù)器值。而方法異常退出時么介,返回地址是要通過異常處理器表來確定的娜遵,棧幀中一般不會保存這部分信息。
方法退出的過程實際上就等同于把當前棧幀出棧壤短,因此退出時可能執(zhí)行的操作有:恢復(fù)上次方法的局部變量表和操作數(shù)棧设拟,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整 PC 計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等久脯。
附加信息
虛擬機規(guī)范允許具體的虛擬機實現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀中纳胧,例如與調(diào)試相關(guān)的信息,這部分信息完全取決于具體的虛擬機實現(xiàn)帘撰。實際開發(fā)中跑慕,一般會把動態(tài)連接、方法返回地址與其他附加信息全部歸為一類摧找,成為棧幀信息核行。
二. 方法調(diào)用
方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個方法)蹬耘,暫時還不涉及方法內(nèi)部的具體運行過程芝雪。
在程序運行時,進行方法調(diào)用是最為普遍综苔、頻繁的操作惩系。前面說過 Class 文件的編譯過程是不包含傳統(tǒng)編譯中的連接步驟的位岔,一切方法調(diào)用在 Class 文件里面存儲的都只是符號引用,而不是方法在運行時內(nèi)存布局中的入口地址(相當于之前說的直接引用)蛆挫。這個特性給 Java 帶來了更強大的動態(tài)擴展能力赃承,但也使得 Java 方法調(diào)用過程變得相對復(fù)雜起來妙黍,需要在類加載期間悴侵,甚至到運行期間才能確定目標方法的直接引用。
解析
所有方法調(diào)用中的目標方法在 Class 文件里都是一個常量池中的符號引用拭嫁,在類加載的解析階段可免,會將其中一部分符號引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是方法在程序真正運行之前就有一個可確定的調(diào)用版本做粤,并且這個方法的調(diào)用版本在運行期是不可改變的浇借。話句話說,調(diào)用目標在程序代碼寫好怕品、編譯器進行編譯時就必須確定下來妇垢。這類方法的調(diào)用稱為解析(Resolution)反惕。
Java 語言中符合「編譯器可知杆麸,運行期不可變」這個要求的方法,主要包括靜態(tài)方法和私有方法兩大類供炼,前者與類型直接關(guān)聯(lián)吼和,后者在外部不可被訪問涨薪,這兩種方法各自的特點決定了它們都不可能通過繼承或者別的方式重寫其它版本,因此它們都適合在類加載階段解析炫乓。
與之相應(yīng)的是刚夺,在 Java 虛擬機里提供了 5 條方法調(diào)用字節(jié)碼指令,分別是:
- invokestatic:調(diào)用靜態(tài)方法末捣;
- invokespecial:調(diào)用實例構(gòu)造器 <init>方法侠姑、私有方法和父類方法;</init>
- invokevirtual:調(diào)用所有虛方法箩做;
- invokeinterface:調(diào)用接口方法结借,會在運行時再確定一個實現(xiàn)此接口的對象;
- invokedynamic:先在運行時動態(tài)解析出調(diào)用點限定符所引用的方法卒茬,然后再執(zhí)行該方法船老。
只要能被 invokestatic 和 invokespecial 指令調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本圃酵,符合這個條件的有靜態(tài)方法柳畔、私有方法、實例構(gòu)造器郭赐、父類方法 4 類薪韩,它們在加載的時候就會把符號引用解析為直接引用确沸。這些方法可以稱為非虛方法,與之相反俘陷,其它方法稱為虛方法(final 方法除外)罗捎。
Java 中的非虛方法除了使用 invokestatic、invokespecial 調(diào)用的方法之外還有一種拉盾,就是被 final 修飾的方法桨菜。雖然 final 方法是使用 invokevirtual 指令來調(diào)用的,但是由于它無法被覆蓋捉偏,沒有其它版本倒得,所以也無需對方法接受者進行多態(tài)選擇,又或者說多態(tài)選擇的結(jié)果肯定是唯一的夭禽。在 Java 語言規(guī)范中明確說明了 final 方法是一種非虛方法霞掺。
解析調(diào)用一定是個靜態(tài)過程,在編譯期間就能完全確定讹躯,在類裝載的解析階段就會把涉及的符號引用全部轉(zhuǎn)變?yōu)榭纱_定的直接引用菩彬,不會延遲到運行期再去完成。而分派(Dispatch)調(diào)用則可能是靜態(tài)的也可能是動態(tài)的潮梯,根據(jù)分派依據(jù)的宗量數(shù)可分為單分派和多分派骗灶。這兩類分派方式的兩兩組合就構(gòu)成了靜態(tài)單分派、靜態(tài)多分派酷麦、動態(tài)單分派矿卑、動態(tài)多分派 4 種分派組合情況,下面我們再看看虛擬機中的方法分派是如何進行的沃饶。
分派
面向?qū)ο笥腥齻€基本特征母廷,封裝、繼承和多態(tài)糊肤。這里要說的分派將會揭示多態(tài)特征的一些最基本的體現(xiàn)琴昆,如「重載」和「重寫」在 Java 虛擬機中是如何實現(xiàn)的?虛擬機是如何確定正確目標方法的馆揉?
靜態(tà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);
}
}
運行后這段程序的輸出結(jié)果如下:
Hello, guy!
Hello, guy!
稍有經(jīng)驗的 Java 程序員都能得出上述結(jié)論,但為什么我們傳遞給 sayHello() 方法的實際參數(shù)類型是 Man 和 Woman升酣,虛擬機在執(zhí)行程序時選擇的卻是 Human 的重載呢舷暮?要理解這個問題,我們先弄清兩個概念噩茄。
Human man = new Man();
上面這段代碼中的「Human」稱為變量的靜態(tài)類型(Static Type)下面,或者叫做外觀類型(Apparent Type),后面的「Man」稱為變量為實際類型(Actual Type)绩聘,靜態(tài)類型和實際類型在程序中都可以發(fā)生一些變化沥割,區(qū)別是靜態(tài)類型的變化僅發(fā)生在使用時耗啦,變量本身的靜態(tài)類型不會被改變,并且最終的靜態(tài)類型是在編譯期可知的机杜;而實際類型變化的結(jié)果在運行期才可確定帜讲,編譯器在編譯程序的時候并不知道一個對象的實際類型是什么。
弄清了這兩個概念椒拗,再來看 StaticDispatch 類中 main() 方法里的兩次 sayHello() 調(diào)用似将,在方法接受者已經(jīng)確定是對象「dispatch」的前提下,使用哪個重載版本陡叠,就完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型玩郊。代碼中定義了兩個靜態(tài)類型相同但是實際類型不同的變量肢执,但是虛擬機(準確的說是編譯器)在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)的枉阵。并且靜態(tài)類型是編譯期可知的,因此在編譯階段预茄, Javac 編譯器會根據(jù)參數(shù)的靜態(tài)類型決定使用哪個重載版本兴溜,所以選擇了 sayHello(Human) 作為調(diào)用目標,并把這個方法的符號引用寫到 man() 方法里的兩條 invokevirtual 指令的參數(shù)中耻陕。
所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作稱為靜態(tài)分派拙徽。靜態(tài)分派的典型應(yīng)用是方法重載。靜態(tài)分派發(fā)生在編譯階段诗宣,因此確定靜態(tài)分派的動作實際上不是由虛擬機來執(zhí)行的膘怕。
另外,編譯器雖然能確定方法的重載版本召庞,但是很多情況下這個重載版本并不是「唯一」的岛心,因此往往只能確定一個「更加合適」的版本。產(chǎn)生這種情況的主要原因是字面量不需要定義篮灼,所以字面量沒有顯示的靜態(tài)類型忘古,它的靜態(tài)類型只能通過語言上的規(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');
}
}
上面代碼的運行結(jié)果為:
Hello, char!
這很好理解髓堪,‘a(chǎn)’ 是一個 char 類型的數(shù)據(jù),自然會尋找參數(shù)類型為 char 的重載方法娘荡,如果注釋掉 sayHello(chat arg) 方法干旁,那么輸出結(jié)果將會變?yōu)椋?/p>
Hello, int!
這時發(fā)生了一次類型轉(zhuǎn)換, ‘a(chǎn)’ 除了可以代表一個字符炮沐,還可以代表數(shù)字 97争群,因為字符 ‘a(chǎn)’ 的 Unicode 數(shù)值為十進制數(shù)字 97,因此參數(shù)類型為 int 的重載方法也是合適的央拖。我們繼續(xù)注釋掉 sayHello(int arg) 方法祭阀,輸出變?yōu)椋?/p>
Hello, long!
這時發(fā)生了兩次類型轉(zhuǎn)換鹉戚,‘a(chǎn)’ 轉(zhuǎn)型為整數(shù) 97 之后,進一步轉(zhuǎn)型為長整型 97L专控,匹配了參數(shù)類型為 long 的重載方法抹凳。我們繼續(xù)注釋掉 sayHello(long arg) 方法,輸出變?yōu)椋?/p>
Hello, Character!
這時發(fā)生了一次自動裝箱伦腐, ‘a(chǎn)’ 被包裝為它的封裝類型 java.lang.Character赢底,所以匹配到了類型為 Character 的重載方法,繼續(xù)注釋掉 sayHello(Character arg) 方法柏蘑,輸出變?yōu)椋?/p>
Hello, Serializable!
這里輸出之所以為「Hello, Serializable!」幸冻,是因為 java.lang.Serializable 是 java.lang.Character 類實現(xiàn)的一個接口,當自動裝箱后發(fā)現(xiàn)還是找不到裝箱類咳焚,但是找到了裝箱類實現(xiàn)了的接口類型洽损,所以緊接著又發(fā)生了一次自動轉(zhuǎn)換。char 可以轉(zhuǎn)型為 int革半,但是 Character 是絕對不會轉(zhuǎn)型為 Integer 的碑定,他只能安全的轉(zhuǎn)型為它實現(xiàn)的接口或父類。Character 還實現(xiàn)了另外一個接口 java.lang.Comparable又官,如果同時出現(xiàn)兩個參數(shù)分別為 Serializable 和 Comparable 的重載方法延刘,那它們在此時的優(yōu)先級是一樣的。編譯器無法確定要自動轉(zhuǎn)型為哪種類型六敬,會提示類型模糊碘赖,拒絕編譯。程序必須在調(diào)用時顯示的指定字面量的靜態(tài)類型外构,如:sayHello((Comparable) ‘a(chǎn)’)普泡,才能編譯通過。繼續(xù)注釋掉 sayHello(Serializable arg) 方法典勇,輸出變?yōu)椋?/p>
Hello, Object!
這時是 char 裝箱后轉(zhuǎn)型為父類了劫哼,如果有多個父類,那將在繼承關(guān)系中從下往上開始搜索割笙,越接近上層的優(yōu)先級越低权烧。即使方法調(diào)用的入?yún)⒅禐?null,這個規(guī)則依然適用伤溉。繼續(xù)注釋掉 sayHello(Serializable arg) 方法般码,輸出變?yōu)椋?/p>
Hello, char...!
7 個重載方法以及被注釋得只剩一個了,可見變長參數(shù)的重載優(yōu)先級是最低的乱顾,這時字符 ‘a(chǎn)’ 被當成了一個數(shù)組元素板祝。
前面介紹的這一系列過程演示了編譯期間選擇靜態(tài)分派目標的過程,這個過程也是 Java 語言實現(xiàn)方法重載的本質(zhì)走净。
動態(tài)分派
動態(tài)分派和多態(tài)性的另一個重要體現(xiàn)「重寫(Override)」有著密切的關(guān)聯(lián)券时,我們依舊通過代碼來理解什么是動態(tài)分派孤里。
/**
* 方法動態(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!
對于上面的代碼,虛擬機是如何確定要調(diào)用哪個方法的呢橘洞?顯然這里不再通過靜態(tài)類型來決定了捌袜,因為靜態(tài)類型同樣都是 Human 的兩個變量 man 和 woman 在調(diào)用 sayHello() 方法時執(zhí)行了不同的行為,并且變量 man 在兩次調(diào)用中執(zhí)行了不同的方法炸枣。導(dǎo)致這個結(jié)果的原因是因為它們的實際類型不同虏等。對于虛擬機是如何通過實際類型來分派方法執(zhí)行版本的,這里我們就不做介紹了适肠,有興趣的可以去看看原著霍衫。
我們把這種在運行期根據(jù)實際類型來確定方法執(zhí)行版本的分派稱為動態(tài)分派。
單分派和多分派
方法的接收者和方法的參數(shù)統(tǒng)稱為方法的宗量侯养,這個定義最早來源于《Java 與模式》一書敦跌。根據(jù)分派基于多少宗量,可將分派劃分為單分派和多分派沸毁。
單分派是根據(jù)一個宗量來確定方法的執(zhí)行版本峰髓;多分派則是根據(jù)多余一個宗量來確定方法的執(zhí)行版本傻寂。
我們依舊通過代碼來理解(代碼以著名的 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!
我們先來看看編譯階段編譯器的選擇過程,也就是靜態(tài)分派過程疾掰。這個時候選擇目標方法的依據(jù)有兩點:一是靜態(tài)類型是 Father 還是 Son搂誉;二是方法入?yún)⑹?QQ 還是 QiHu360。因為是根據(jù)兩個宗量進行選擇的静檬,所以 Java 語言的靜態(tài)分派屬于多分派炭懊。
再看看運行階段虛擬機的選擇過程,也就是動態(tài)分派的過程拂檩。在執(zhí)行 son.hardChoice(new QiHu360()) 時侮腹,由于編譯期已經(jīng)確定目標方法的簽名必須為 hardChoice(QiHu360),這時參數(shù)的靜態(tài)類型稻励、實際類型都不會對方法的選擇造成任何影響父阻,唯一可以影響虛擬機選擇的因數(shù)只有此方法的接收者的實際類型是 Father 還是 Son。因為只有一個宗量作為選擇依據(jù)望抽,所以 Java 語言的動態(tài)分派屬于單分派加矛。
綜上所述,Java 語言是一門靜態(tài)多分派煤篙、動態(tài)單分派的語言斟览。
三. 基于棧的字節(jié)碼解釋執(zhí)行引擎
虛擬機如何調(diào)用方法已經(jīng)介紹完了,下面我們來看看虛擬機是如何執(zhí)行方法中的字節(jié)碼指令的辑奈。
解釋執(zhí)行
Java 語言常被人們定義成「解釋執(zhí)行」的語言苛茂,但隨著 JIT 以及可直接將 Java 代碼編譯成本地代碼的編譯器的出現(xiàn)已烤,這種說法就不對了。只有確定了談?wù)搶ο笫悄撤N具體的 Java 實現(xiàn)版本和執(zhí)行引擎運行模式時妓羊,談解釋執(zhí)行還是編譯執(zhí)行才會比較確切草戈。
無論是解釋執(zhí)行還是編譯執(zhí)行,無論是物理機還是虛擬機侍瑟,對于應(yīng)用程序唐片,機器都不可能像人一樣閱讀、理解涨颜,然后獲得執(zhí)行能力费韭。大部分的程序代碼到物理機的目標代碼或者虛擬機執(zhí)行的指令之前,都需要經(jīng)過下圖中的各個步驟庭瑰。下圖中最下面的那條分支星持,就是傳統(tǒng)編譯原理中程序代碼到目標機器代碼的生成過程;中間那條分支弹灭,則是解釋執(zhí)行的過程督暂。
如今,基于物理機穷吮、Java 虛擬機或者非 Java 的其它高級語言虛擬機的語言逻翁,大多都會遵循這種基于現(xiàn)代編譯原理的思路,在執(zhí)行前先對程序源代碼進行詞法分析和語法分析處理捡鱼,把源代碼轉(zhuǎn)化為抽象語法樹八回。對于一門具體語言的實現(xiàn)來說,詞法分析驾诈、語法分析以至后面的優(yōu)化器和目標代碼生成器都可以選擇獨立于執(zhí)行引擎缠诅,形成一個完整意義的編譯器去實現(xiàn),這類代表是 C/C++乍迄。也可以為一個半獨立的編譯器管引,這類代表是 Java。又或者把這些步驟和執(zhí)行全部封裝在一個封閉的黑匣子中闯两,如大多數(shù)的 JavaScript 執(zhí)行器褥伴。
Java 語言中,Javac 編譯器完成了程序代碼經(jīng)過詞法分析生蚁、語法分析到抽象語法樹噩翠、再遍歷語法樹生成字節(jié)碼指令流的過程。因為這一部分動作是在 Java 虛擬機之外進行的邦投,而解釋器在虛擬機的內(nèi)部伤锚,所以 Java 程序的編譯就是半獨立的實現(xiàn)。
許多 Java 虛擬機的執(zhí)行引擎在執(zhí)行 Java 代碼的時候都有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇。而對于最新的 Android 版本的執(zhí)行模式則是 AOT + JIT + 解釋執(zhí)行屯援,關(guān)于這方面我們后面有機會再聊猛们。
基于棧的指令集與基于寄存器的指令集
Java 編譯器輸出的指令流,基本上是一種基于棧的指令集架構(gòu)狞洋⊥涮裕基于棧的指令集主要的優(yōu)點就是可移植,寄存器由硬件直接提供吉懊,程序直接依賴這些硬件寄存器則不可避免的要受到硬件約束庐橙。棧架構(gòu)的指令集還有一些其他優(yōu)點,比如相對更加緊湊(字節(jié)碼中每個字節(jié)就對應(yīng)一條指令借嗽,而多地址指令集中還需要存放參數(shù))态鳖、編譯實現(xiàn)更加簡單(不需要考慮空間分配的問題,所有空間都是在棧上操作)等恶导。
棧架構(gòu)指令集的主要缺點是執(zhí)行速度相對來說會稍慢一些浆竭。所有主流物理機的指令集都是寄存器架構(gòu)也從側(cè)面印證了這一點。
雖然棧架構(gòu)指令集的代碼非常緊湊惨寿,但是完成相同功能需要的指令集數(shù)量一般會比寄存器架構(gòu)多邦泄,因為出棧、入棧操作本身就產(chǎn)生了相當多的指令數(shù)量裂垦。更重要的是顺囊,棧實現(xiàn)在內(nèi)存中,頻繁的棧訪問也意味著頻繁的內(nèi)存訪問缸废,相對于處理器來說包蓝,內(nèi)存始終是執(zhí)行速度的瓶頸。由于指令數(shù)量和內(nèi)存訪問的原因企量,所以導(dǎo)致了棧架構(gòu)指令集的執(zhí)行速度會相對較慢。
正是基于上述原因亡电,Android 虛擬機中采用了基于寄存器的指令集架構(gòu)届巩。不過有一點不同的是,前面說的是物理機上的寄存器份乒,而 Android 上指的是虛擬機上的寄存器恕汇。