[TOC]
8.1 概述
- 執(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èn)洌踔吝€可能會包含幾個不同級別的編譯器執(zhí)行引擎伴逸。
- 但從外觀上看起來缠沈,所有的 Java 虛擬機的執(zhí)行引擎都是一致的:輸入的是字節(jié)碼文件,處理過程是字節(jié)碼解析的等效過程错蝴,輸出的是執(zhí)行結(jié)果洲愤。
8.2 運行時棧幀結(jié)構(gòu)
8.2.0 概述
- 棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧(Virtual Machine Stack)的棧元素顷锰。
- 棧幀存儲了方法的局部變量表柬赐、操作數(shù)棧、動態(tài)連接和方法返回地址等信息官紫。每一個方法從調(diào)用開始至執(zhí)行完成的過程躺率,都對應(yīng)著一個棧幀在虛擬機棧里面從入棧到出棧的過程玛界。(一個方法對應(yīng)一個棧幀)
- 在編譯程序代碼的時候,棧幀中需要多大的局部變量表悼吱,多深的操作數(shù)棧都已經(jīng)完全確定了慎框,并且寫入到方法表的 Code 屬性之中,因此一個棧幀需要分配多少內(nèi)存后添,不會受到程序運行期變量數(shù)據(jù)的影響笨枯,而僅僅取決于具體的虛擬機實現(xiàn)。
- 一個線程中的方法調(diào)用鏈可能會很長遇西,很多方法都同時處于執(zhí)行狀態(tài)馅精。對于執(zhí)行引擎來說,在活動線程中粱檀,只有位于棧頂的棧幀才是有效的洲敢,稱為當(dāng)前棧幀(Current Stack Frame),與這個棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)茄蚯。執(zhí)行引擎運行的所有字節(jié)碼指令都只針對當(dāng)前棧幀進行操作压彭。
8.2.1 局部變量表
- 局部變量表(Local-Variable-Table)是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量渗常。
- 在 Java 程序編譯為 Class 文件時壮不,就在方法的 Code 屬性的
max_locals
數(shù)據(jù)項中確定了該方法所需要分配的局部變量表的最大容量。 - 局部變量表的容量以變量槽(Variable Slot皱碘,下稱Slot)為最小單位询一,虛擬機規(guī)范中并沒有明確指明一個Slot應(yīng)占用的內(nèi)存空間大小,只是很有導(dǎo)向性地說到每個Slot都應(yīng)該能存放一個boolean癌椿、byte健蕊、char、short踢俄、int绊诲、float、reference 或returnAddress類型的數(shù)據(jù)褪贵。
- 對于64位的數(shù)據(jù)類型,虛擬機會以高位對齊的方式為其分配兩個連續(xù)的Slot空間抗俄。
- 虛擬機通過索引定位的方式使用局部變量表脆丁,索引值的范圍是從0開始至局部變量表最大的Slot數(shù)量。如果訪問的是32位數(shù)據(jù)類型的變量动雹,索引 n 就代表了使用第n個Slot槽卫,如果是64位數(shù)據(jù)類型的變量,則說明會同時使用n和n+1兩個Slot胰蝠。對于兩個相鄰的共同存放一個64位數(shù)據(jù)的兩個Slot歼培,不允許采用任何方式單獨訪問其中的某一個震蒋,Java虛擬機規(guī)范中明確要求了如果遇到進行這種操作的字節(jié)碼序列,虛擬機應(yīng)該在類加載的校驗階段拋出異常躲庄。
- 在方法執(zhí)行時查剖,虛擬機是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程的,如果執(zhí)行的是實例方法(非static的方法)噪窘,那局部變量表中第0位索引的Slot默認(rèn)是用于傳遞方法所屬對象實例的引用笋庄,在方法中可以通過關(guān)鍵字 “this” 來訪問到這個隱含的參數(shù)。其余參數(shù)則按照參數(shù)表順序排列倔监,占用從1開始的局部變量Slot直砂,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot浩习。
- 為了盡可能節(jié)省棧幀空間静暂,局部變量表中的Slot是可以重用的,方法體中定義的變量谱秽,其作用域并不一定會覆蓋整個方法體洽蛀,如果當(dāng)前字節(jié)碼PC計數(shù)器的值已經(jīng)超出了某個變量的作用域,那這個變量對應(yīng)的Slot就可以交給其他變量使用弯院。
8.2.2 操作數(shù)棧
- 操作數(shù)棧(Operand Stack)也常稱為操作棧辱士,它是一個后入先出(Last In First Out,LIFO)棧听绳。
- 操作數(shù)棧的每一個元素可以是任意的 Java 數(shù)據(jù)類型颂碘,包括 long 和double。32位數(shù)據(jù)類型所占的棧容量為1椅挣,64位數(shù)據(jù)類型所占的棧容量為2头岔。
- 操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴(yán)格匹配,在編譯程序代碼的時候鼠证,編譯器要嚴(yán)格保證這一點峡竣,在類校驗階段的數(shù)據(jù)流分析中還要再次驗證這一點。
- 以 iadd 指令為例量九,這個指令用于整型數(shù)加法适掰,它在執(zhí)行時,最接近棧頂?shù)膬蓚€元素的數(shù)據(jù)類型必須為 int 型荠列,不能出現(xiàn)一個 long 和一個 float 使用 iadd 命令相加的情況类浪。
- 另外,在概念模型中肌似,兩個棧幀作為虛擬機棧的元素费就,是完全相互獨立的。但在大多虛擬機的實現(xiàn)里都會做一些優(yōu)化處理川队,令兩個棧幀出現(xiàn)一部分重疊力细。讓下面棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起睬澡,這樣在進行方法調(diào)用時就可以共用一部分?jǐn)?shù)據(jù),無須進行額外的參數(shù)復(fù)制傳遞眠蚂。
8.2.3 動態(tài)連接
- 每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用煞聪,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接(Dynamic Linking)。
- Class文件的常量池中存有大量的符號引用河狐,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用作為參數(shù)米绕。
- 靜態(tài)解析:符號引用在類加載階段或者第一次使用的時候轉(zhuǎn)化為直接引用
- 動態(tài)連接:符號引用在每一次運行期間轉(zhuǎn)化為直接引用。
8.2.4 方法返回地址
- 當(dāng)一個方法開始執(zhí)行后馋艺,只有兩種方式可以退出這個方法栅干。
- 第一種方式:執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令,這時候可能會有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者)捐祠,是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定碱鳞,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)。
- 另外一種退出方式:在方法執(zhí)行過程中遇到了異常踱蛀,并且這個異常沒有在方法體內(nèi)得到處理窿给,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導(dǎo)致方法退出率拒,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)崩泡。
- 一個方法使用異常完成出口的方式退出,是不會給它的上層調(diào)用者產(chǎn)生任何返回值的猬膨。
- 無論采用何種退出方式角撞,在方法退出之后,都需要返回到方法被調(diào)用的位置勃痴,程序才能繼續(xù)執(zhí)行谒所,方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)沛申。
- 一般來說劣领,方法正常退出時,調(diào)用者的 PC 計數(shù)器的值可以作為返回地址铁材,棧幀中很可能會保存這個計數(shù)器值尖淘。
- 而方法異常退出時,返回地址是要通過異常處理器表來確定的著觉,棧幀中一般不會保存這部分信息村生。方法退出的過程實際上就等同于把當(dāng)前棧幀出棧,因此退出時可能執(zhí)行的操作有:
- 恢復(fù)上層方法的局部變量表和操作數(shù)棧
- 把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中
- 調(diào)整 PC 計數(shù)器的值以指向方法調(diào)用指令后面的一條指令等固惯。
8.3 方法調(diào)用
- 方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個方法)缴守,暫時還不涉及方法內(nèi)部的具體運行過程葬毫。
- 在程序運行時镇辉,進行方法調(diào)用是最普遍、最頻繁的操作贴捡,但前面已經(jīng)講過忽肛,Class文件的編譯過程中不包含傳統(tǒng)編譯中的連接步驟,一切方法調(diào)用在Class文件里面存儲的都只是符號引用烂斋,而不是方法在實際運行時內(nèi)存布局中的入口地址(相當(dāng)于之前說的直接引用)屹逛。
8.3.1 解析
- 所有方法調(diào)用中的目標(biāo)方法在Class文件里面都是一個常量池中的符號引用,在類加載的解析階段汛骂,會將其中的一部分符號引用轉(zhuǎn)化為直接引用罕模,這種解析能成立的前提是:方法在程序真正運行之前就有一個可確定的調(diào)用版本,并且這個方法的調(diào)用版本在運行期是不可改變的帘瞭。換句話說淑掌,調(diào)用目標(biāo)在程序代碼寫好、編譯器進行編譯時就必須確定下來蝶念。這類方法的調(diào)用稱為解析(Resolution)抛腕。
- 在 Java 語言中符合 “編譯期可知,運行期不可變” 這個要求的方法媒殉,主要包括靜態(tài)方法和私有方法兩大類担敌,前者與類型直接關(guān)聯(lián),后者在外部不可被訪問廷蓉。
- 只要能被 invokestatic 和 invokespecial 指令調(diào)用的方法全封,都可以在解析階段中確定唯一的調(diào)用版本,符合這個條件的有靜態(tài)方法苦酱、私有方法售貌、實例構(gòu)造器、父類方法 4類疫萤,它們在類加載的時候就會把符號引用解析為該方法的直接引用颂跨。這些方法可以稱為非虛方法,與之相反扯饶,其他方法稱為虛方法(除去 final 方法)
- 雖然 final 方法是使用 invokevirtual 指令來調(diào)用的恒削,但是由于它無法被覆蓋,沒有其他版本尾序,所以也無須對方法接收者進行多態(tài)選擇钓丰,又或者說多態(tài)選擇的結(jié)果肯定是唯一的。
- 解析調(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種分派組合情況。
8.3.2 分派
8.3.2.1 靜態(tài)分派
public class StaticDispatch
{
static abstract class Human
{
}
static class Man extends Human
{
}
static class Woman extends Human
{
}
public void sayHello(Human guy)
{
System.out.println("hello,guy!");
}
public void sayHello(Man guy)
{
System.out.println("hello,gentleman!");
}
public void sayHello(Woman guy)
{
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) man);
sr.sayHello((Woman) woman);
}
}
- 上面代碼中的 “Human” 稱為變量的靜態(tài)類型(Static Type)存筏,或者叫做外觀類型(Apparent Type)宠互,后面的 “Man” 則稱為變量的實際類型(ActualType)
- 靜態(tài)類型和實際類型在程序中都可以發(fā)生一些變化,區(qū)別是靜態(tài)類型的變化僅僅在使用時發(fā)生椭坚,變量本身的靜態(tài)類型不會被改變予跌,并且最終的靜態(tài)類型是在編譯期可知的
- 而實際類型變化的結(jié)果在運行期才可確定,編譯器在編譯程序的時候并不知道一個對象的實際類型是什么藕溅。
-
main()
里面的兩次sayHello()
方法調(diào)用匕得,在方法接收者已經(jīng)確定是對象 “sr” 的前提下,使用哪個重載版本巾表,就完全取決于傳入?yún)?shù)的數(shù)量和數(shù)據(jù)類型汁掠。代碼中刻意地定義了兩個靜態(tài)類型相同但實際類型不同的變量,但虛擬機(準(zhǔn)確地說是編譯器)在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)的集币。 - 因此考阱,在編譯階段,Javac 編譯器會根據(jù)參數(shù)的靜態(tài)類型決定使用哪個重載版本鞠苟,所以選擇了
sayHello (Human)
作為調(diào)用目標(biāo)乞榨,并把這個方法的符號引用寫到main()
方法里的兩條 invokevirtual 指令的參數(shù)中。 - 所有依賴靜態(tài)類型來定位方法執(zhí)行版本的分派動作稱為靜態(tài)分派当娱。靜態(tài)分派的典型應(yīng)用是方法重載吃既。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動作實際上不是由虛擬機來執(zhí)行的跨细。
- 另外鹦倚,編譯器雖然能確定出方法的重載版本,但在很多情況下這個重載版本并不是 “唯一的” 冀惭,往往只能確定一個 “更加合適的” 版本震叙。這種模糊的結(jié)論在由0和1構(gòu)成的計算機世界中算是比較 “稀罕” 的事情,產(chǎn)生這種模糊結(jié)論的主要原因是字面量不需要定義散休,所以字面量沒有顯式的靜態(tài)類型媒楼,它的靜態(tài)類型只能通過語言上的規(guī)則去理解和推斷。
8.3.2.2 動態(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();
}
}
- 顯然這里不可能再根據(jù)靜態(tài)類型來決定戚丸,因為靜態(tài)類型同樣都是Human的兩個變量man和woman在調(diào)用
sayHello()
方法時執(zhí)行了不同的行為划址,并且變量 man 在兩次調(diào)用中執(zhí)行了不同的方法。 - 導(dǎo)致這個現(xiàn)象的原因很明顯,是這兩個變量的實際類型不同夺颤。
- 原因要從 invokevirtual 指令的多態(tài)查找過程開始說起对人,invokevirtual指令的運行時解析過程大致分為以下幾個步驟:
- 找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記作 C拂共。
- 如果在類型 C 中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權(quán)限校驗姻几,如果通過則返回這個方法的直接引用宜狐,查找過程結(jié)束;如果不通過蛇捌,則返回 java.lang.IlegalAccessError 異常抚恒。
- 否則,按照繼承關(guān)系從下往上依次對 C 的各個父類進行第 2 步的搜索和驗證過程络拌。
- 如果始終沒有找到合適的方法俭驮,則拋出 java.lang.AbstractMethodError 異常。
- 由于invokevirtual 指令執(zhí)行的第一步就是在運行期確定接收者的實際類型春贸,所以兩次調(diào)用中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上混萝,這個過程就是Java語言中方法重寫的本質(zhì)。
8.3.2.3 單分派與多分派
- 方法的接收者與方法的參數(shù)統(tǒng)稱為方法的宗量萍恕,根據(jù)分派基于多少種宗量逸嘀,可以將分派劃分為單分派和多分派兩種。
- 單分派是根據(jù)一個宗量對目標(biāo)方法進行選擇允粤,多分派則是根據(jù)多于一個宗量對目標(biāo)方法進行選擇崭倘。
public class Dispatch
{
static class QQ {}
static class _360 {}
public static class Father
{
public void hardChoice(QQ arg)
{
System.out.printin("father choose qq");
}
public void hardchoice(_360 arg)
{
System.out.print1n("father choose 360");
}
}
public static class Son extends Father
{
public void hardChoice(QQ arg)
{
System.out.printin("son choose qq");
}
public void hardchoice(_360 arg)
{
System.out.print1n("son choose 360");
}
}
public static void main(String args[])
{
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
-
我們來看看編譯階段編譯器的選擇過程,也就是靜態(tài)分派的過程类垫。這時選擇目標(biāo)方法的依據(jù)有兩點:
- 一是靜態(tài)類型是Father還是Son司光,
- 二是方法參數(shù)是 QQ 還是 360。
這次選擇結(jié)果的最終產(chǎn)物是產(chǎn)生了兩條 invokevirtual 指令悉患,兩條指令的參數(shù)分別為常量池中指向
Father.hardChoice(360)
及Father.hardChoice(QQ)
方法的符號引用残家。因為是根據(jù)兩個宗量進行選擇,所以 Java 語言的靜態(tài)分派屬于多分派類型购撼。-
再看看運行階段虛擬機的選擇跪削,也就是動態(tài)分派的過程:
- 在執(zhí)行
som.hardChoice (new QQ)
這句代碼時,更準(zhǔn)確地說迂求,是在執(zhí)行這句代碼所對應(yīng)的 invokevirtual 指令時碾盐,由于編譯期已經(jīng)決定目標(biāo)方法的簽名必須為hardChoice(QQ)
,因此這時參數(shù)的靜態(tài)類型揩局、實際類型都對方法的選擇不會構(gòu)成任何影響毫玖。 - 唯一可以影響虛擬機選擇的因素只有此方法的接受者的實際類型是 Father 還是 Son。因為只有一個宗量作為選擇依據(jù),所以 Java 語言的動態(tài)分派屬于單分派類型付枫。
- 在執(zhí)行
根據(jù)上述論證的結(jié)果烹玉,我們可以總結(jié)一句:今天的 Java 語言是一門靜態(tài)多分派、動態(tài)單分派的語言阐滩。
8.3.2.4 虛擬機動態(tài)分派的實現(xiàn)
- 由于動態(tài)分派是非常頻繁的動作二打,而且動態(tài)分派的方法版本選擇過程需要運行時在類的方法元數(shù)據(jù)中搜索合適的目標(biāo)方法,因此在虛擬機的實際實現(xiàn)中基于性能的考慮掂榔,大部分實現(xiàn)都不會真正地進行如此頻繁的搜索继效。
- 面對這種情況,最常用的 “穩(wěn)定優(yōu)化” 手段就是為類在方法區(qū)中建立一個虛方法表(Vritual Method Table装获,也稱為 vtable瑞信,與此對應(yīng)的,在 invokeinterface 執(zhí)行時也會用到接口方法表——Inteface Method Table穴豫,簡稱 itable)凡简,使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。
-
虛方法表中存放著各個方法的實際入口地址精肃。
- 如果某個方法在子類中沒有被重寫秤涩,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現(xiàn)入口司抱。
- 如果子類中重寫了這個方法溉仑,子類方法表中的地址將會替換為指向子類實現(xiàn)版本的入口地址。
方法表一般在類加載的連接階段進行初始化状植,準(zhǔn)備了類的變量初始值后浊竟,虛擬機會把該類的方法表也初始化完畢。
方法表是分派調(diào)用的 “穩(wěn)定優(yōu)化” 手段津畸,虛擬機除了使用方法表之外振定,在條件允許的情況下,還會使用內(nèi)聯(lián)緩存(Inline Cache)和基于 “類型繼承關(guān)系分析”(Class Hierarchy Analysis肉拓,CHA)技術(shù)的守護內(nèi)聯(lián)(Guarded Inlining)兩種非穩(wěn)定的 “激進優(yōu)化” 手段來獲得更高的性能后频。
8.4 基于棧的字節(jié)碼解釋執(zhí)行引擎
8.4.1 解釋執(zhí)行
- 大部分的程序代碼到物理機的目標(biāo)代碼或虛擬機能執(zhí)行的指令集之前,都需要經(jīng)過下圖的各個步驟暖途。圖中下面那條分支卑惜,就是傳統(tǒng)編譯原理中程序代碼到目標(biāo)機器代碼的生成過程,而中間的那條分支驻售,自然就是解釋執(zhí)行的過程露久。(綠色代表可選項)
-
如今,基于物理機欺栗、Java虛擬機毫痕,或者非Java的其他高級語言虛擬機(HLLVM)的語言征峦,大多都會遵循這種基于現(xiàn)代經(jīng)典編譯原理的思路:
- 在執(zhí)行前先對程序源碼進行詞法分析和語法分析處理,把源碼轉(zhuǎn)化為抽象語法樹(Abstract Syntax Tree消请,AST)
- 對于一門具體語言的實現(xiàn)來說栏笆,詞法分析、語法分析以至后面的優(yōu)化器和目標(biāo)代碼生成器都可以選擇獨立于執(zhí)行引擎臊泰,形成一個完整意義的編譯器去實現(xiàn)蛉加,這類代表是C/C++語言。
- 也可以選擇把其中一部分步驟(如生成抽象語法樹之前的步驟)實現(xiàn)為一個半獨立的編譯器缸逃,這類代表是Java語言七婴。
- 又或者把這些步驟和執(zhí)行引擎全部集中封裝在一個封閉的黑匣子之中,如大多數(shù)的 JavaScript 執(zhí)行器察滑。
Java 語言中,Javac 編譯器完成了程序代碼經(jīng)過詞法分析修肠、語法分析到抽象語法樹贺辰,再遍歷語法樹生成線性的字節(jié)碼指令流的過程。因為這一部分動作是在 Java 虛擬機之外進行的嵌施,而解釋器在虛擬機的內(nèi)部饲化,所以 Java 程序的編譯就是半獨立的實現(xiàn)。
8.4.2 基于棧的指令集與基于寄存器的指令集
- Java 編譯器輸出的指令流吗伤,基本上是一種基于棧的指令集架構(gòu)(Instruction Set Architecture吃靠,ISA),指令流中的指令大部分都是零地址指令足淆,它們依賴操作數(shù)棧進行工作巢块。
- 與之相對的另外一套常用的指令集架構(gòu)是基于寄存器的指令集,最典型的就是 x86 的二地址指令集巧号,說得通俗一些族奢,就是現(xiàn)在我們主流PC機中直接支持的指令集架構(gòu),這些指令依賴寄存器進行工作丹鸿。
- 基于棧的指令集主要的優(yōu)點
- 可移植越走。因為寄存器由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免地要受到硬件的約束靠欢。如果使用棧架構(gòu)的指令集廊敌,用戶程序不會直接使用這些寄存器,就可以由虛擬機實現(xiàn)來自行決定把一些訪問最頻繁的數(shù)據(jù)(程序計數(shù)器门怪、棧頂緩存等)放到寄存器中以獲取盡量好的性能骡澈,這樣實現(xiàn)起來也更加簡單一些。
- 代碼相對更加緊湊(字節(jié)碼中每個字節(jié)就對應(yīng)一條指令掷空,而多地址指令集中還需要存放參數(shù))
- 編譯器實現(xiàn)更加簡單(不需要考慮空間分配的問題秧廉,所需空間都在棧上操作)等伞广。
- 棧架構(gòu)指令集的主要缺點是執(zhí)行速度相對來說會稍慢一些。所有主流物理機的指令集都是寄存器架構(gòu)也從側(cè)面印證了這一點疼电。
- 雖然棧架構(gòu)指令集的代碼非常緊湊嚼锄,但是完成相同功能所需的指令數(shù)量一般會比寄存器架構(gòu)多,因為出棧蔽豺、入棧操作本身就產(chǎn)生了相當(dāng)多的指令數(shù)量区丑。更重要的是,棧實現(xiàn)在內(nèi)存之中修陡,頻繁的棧訪問也就意味著頻繁的內(nèi)存訪問沧侥,相對于處理器來說,內(nèi)存始終是執(zhí)行速度的瓶頸魄鸦。盡管虛擬機可以采取棧頂緩存的手段宴杀,把最常用的操作映射到寄存器中避免直接內(nèi)存訪問,但這也只能是優(yōu)化措施而不是解決本質(zhì)問題的方法拾因。