????????執(zhí)行引擎是 Java 虛擬機(jī)最核心的組成部分之一厨相×镀撸“虛擬機(jī)” 是一個(gè)相對(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)一外觀(Facade)。在不同的虛擬機(jī)實(shí)現(xiàn)里面蚯涮,執(zhí)行引擎在執(zhí)行 Java? 代碼的時(shí)候可能會(huì)有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時(shí)編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇治专,也可能兩者兼?zhèn)洌踔吝€可能會(huì)包含幾個(gè)不同級(jí)別的編譯器執(zhí)行引擎遭顶。但從外觀上看起來张峰,所有的 Java 虛擬機(jī)的執(zhí)行引擎都是一致的:輸入的是字節(jié)碼文件,處理過程是字節(jié)碼解析的等效過程棒旗,輸出的是執(zhí)行結(jié)果喘批,下面將主要從概念模型的角度來講解虛擬機(jī)的方法調(diào)用和字節(jié)碼執(zhí)行。
運(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ī)棧(Virtual Machine Stack)的棧元素饶深。棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧逛拱、動(dòng)態(tài)連接和方法返回地址等信息敌厘。每一個(gè)方法從調(diào)用開始至執(zhí)行完成的過程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過程朽合。
????????每一個(gè)棧幀都包括了局部變量表俱两、操作數(shù)棧、動(dòng)態(tài)連接曹步、方法返回地址和一些額外的附加信息宪彩。在編譯程序代碼的時(shí)候,棧幀中需要多大的局部變量表讲婚,多深的操作數(shù)棧都已經(jīng)完全確定了尿孔,并且寫入到方法表的 Code 屬性之中,因此一個(gè)棧幀需要分配多少內(nèi)存筹麸,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響活合,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。
??????? 一個(gè)線程中的方法調(diào)用鏈可能會(huì)很長竹捉,很多方法都同時(shí)處于執(zhí)行狀態(tài)芜辕。對(duì)于執(zhí)行引擎來說,在活動(dòng)線程中块差,只有位于棧頂?shù)臈攀怯行У?/b>侵续,稱為當(dāng)前棧幀(Current Stack Frame)倔丈,與這個(gè)棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作状蜗,在概念模型上需五,典型的棧幀結(jié)構(gòu)如圖 8-1 所示。
??????? 接下來詳細(xì)講解一下棧幀中的局部變量表轧坎、操作數(shù)棧宏邮、動(dòng)態(tài)連接、方法返回地址等各個(gè)部分的作用和數(shù)據(jù)結(jié)構(gòu)缸血。
局部變量表
????????局部變量表(Local Variable Table) 是一組變量值存儲(chǔ)空間蜜氨,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。在 Java 程序編譯為 Class 文件時(shí)捎泻,就在方法的 Code 屬性的 max_locals 數(shù)據(jù)項(xiàng)中確定了該方法所需要分配的局部變量表的最大容量飒炎。
????????局部變量表的容量以變量槽(Variable Slot,下稱 Slot)為最小單位笆豁,虛擬機(jī)規(guī)范中并沒有明確指明一個(gè) Slot 應(yīng)占用的內(nèi)存空間大小郎汪,只是很有導(dǎo)向性地說到每個(gè) Slot 都應(yīng)該能存放一個(gè) boolean、byte闯狱、char煞赢、short、int哄孤、float照筑、reference (注:Java 虛擬機(jī)規(guī)范中沒有明確規(guī)定 reference 類型的長度,它的長度與實(shí)際使用 32 還是 64 位虛擬機(jī)有關(guān)录豺,如果是 64 位虛擬機(jī)朦肘,還與是否開啟某些對(duì)象指針壓縮的優(yōu)化有關(guān)饭弓,這里暫且只取 32 位虛擬機(jī)的 reference 長度)或 returnAddress 類型的數(shù)據(jù)双饥,這 8 種數(shù)據(jù)類型,都可以使用 32 位或更小的物理內(nèi)存來存放弟断,但這種描述與明確指出 “每個(gè) Slot 占用 32 位長度的內(nèi)存空間” 是有一些差別的咏花,它允許 Slot 的長度可以隨著處理器、操作系統(tǒng)或虛擬機(jī)的不同而發(fā)送變化阀趴。只要保證即使在 64 位虛擬機(jī)中使用了 64 位的物理內(nèi)存空間去實(shí)現(xiàn)一個(gè) Slot昏翰,虛擬機(jī)仍要使用對(duì)齊和補(bǔ)白的手段讓 Slot 在外觀上看起來與 32 位虛擬機(jī)中的一致。
????????既然前面提到了 Java 虛擬機(jī)的數(shù)據(jù)類型刘急,在此再簡單介紹一下它們棚菊。一個(gè) Slot 可以存放一個(gè) 32 位以內(nèi)的數(shù)據(jù)類型,Java 中占用 32 位以內(nèi)的數(shù)據(jù)類型有 boolean叔汁、byte统求、char检碗、short、int码邻、float折剃、reference 和 returnAddress 8 種類型。前面 6 種不需要多加解釋像屋,讀者可以按照 Java 語言中對(duì)應(yīng)數(shù)據(jù)類型的概念去理解它們(僅是這樣理解而已怕犁,Java 語言與 Java 虛擬機(jī)中的基本數(shù)據(jù)類型是存在本質(zhì)差別的),而第 7 種 reference 類型表示對(duì)一個(gè)對(duì)象實(shí)例的引用己莺,虛擬機(jī)規(guī)范既沒有說明他的長度奏甫,也沒有明確指出這種引用應(yīng)有怎樣的結(jié)構(gòu)。但一般來說凌受,虛擬機(jī)實(shí)現(xiàn)至少都應(yīng)當(dāng)能通過這個(gè)引用做到兩點(diǎn)扶檐,一是從此引用中直接或間接地查找到對(duì)象在 Java 堆中的數(shù)據(jù)存放的起始地址索引,二是此引用中直接或間接地查找到對(duì)象所屬數(shù)據(jù)類型在方法區(qū)中的存儲(chǔ)的類型信息胁艰,否則無法實(shí)現(xiàn) Java 語言規(guī)范中定義的語法約束約束款筑。第 8 種即 returnAddress 類型目前已經(jīng)很少見了,它是為字節(jié)碼指令 jsr腾么、jsr_w 和 ret 服務(wù)的奈梳,指向了一條字節(jié)碼指令的地址,很古老的 Java 虛擬機(jī)曾經(jīng)使用這幾條指令來實(shí)現(xiàn)異常處理解虱,現(xiàn)在已經(jīng)由異常表代替攘须。
????????對(duì)于 64 位的數(shù)據(jù)類型,虛擬機(jī)會(huì)以高位對(duì)齊的方式為其分配兩個(gè)連續(xù)的 Slot 空間殴泰。Java 語言中明確的(reference 類型則可能是 32 位也可能是 64 位)64 位的數(shù)據(jù)類型只有 long 和 double 兩種于宙。值得一提的是,這里把 long 和 double 數(shù)據(jù)類型分割存儲(chǔ)的做法與 “l(fā)ong 和 double 非原子性協(xié)定” 中把一次 long 和 double 數(shù)據(jù)類型讀寫分割為兩次 32 位讀寫的做法有些類似悍汛,讀者閱讀到 Java 內(nèi)存模型時(shí)可以互相對(duì)比一下捞魁。不過,由于局部變量建立在線程的堆棧上离咐,是線程私有的數(shù)據(jù)谱俭,無論讀寫兩個(gè)連續(xù)的 Slot 是否為原子操作,都不會(huì)引起數(shù)據(jù)安全問題宵蛀。
????????虛擬機(jī)通過索引定位的方式使用局部變量表昆著,索引值的范圍是從 0 開始至局部變量表最大的 Slot 數(shù)量。如果訪問的是 32 位數(shù)據(jù)類型的變量术陶,索引 n 就代表了使用第 n 個(gè) Slot凑懂,如果是 64 位數(shù)據(jù)類型的變量,則說明會(huì)同時(shí)使用 n 和 n+1 兩個(gè) Slot梧宫。對(duì)于兩個(gè)相鄰的共同存放一個(gè) 64 位數(shù)據(jù)的兩個(gè) Slot接谨,不允許采用任何方式單獨(dú)訪問其中的某一個(gè)杭攻,Java 虛擬機(jī)規(guī)范中明確要求了如果遇到進(jìn)行這種操作的字節(jié)碼序列,虛擬機(jī)應(yīng)該在類加載的校驗(yàn)階段拋出異常疤坝。
??????? 在方法執(zhí)行時(shí)兆解,虛擬機(jī)是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程的,如果執(zhí)行的是實(shí)例方法(非 static 的方法)跑揉,那局部變量表中第 0 位索引的 Slot 默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用锅睛,在方法中可以通過關(guān)鍵字 “this” 來訪問到這個(gè)隱含的參數(shù)。其余參數(shù)則按照參數(shù)表順序排列历谍,占用從 1 開始的局部變量 Slot现拒,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的 Slot望侈。
?????????為了盡可能節(jié)省棧幀空間印蔬,局部變量中的 Slot 是可以重用的,方法體中定義的變量脱衙,其作用域并不一定會(huì)覆蓋整個(gè)方法體侥猬,如果當(dāng)前字節(jié)碼 PC 計(jì)數(shù)器的值已經(jīng)超出了某個(gè)變量的作用域,那這個(gè)變量對(duì)應(yīng)的 Slot 就可以交給其他變量使用捐韩。不過退唠,這樣的設(shè)計(jì)除了節(jié)省棧幀空間以外,還會(huì)伴隨一些額外的副作用.
? ??????Java 語言的一本著名書籍《Practical Java》中把 “不使用的對(duì)象應(yīng)手動(dòng)賦值為 null” 作為一條推薦的編碼規(guī)則荤胁。筆者的觀點(diǎn)是不應(yīng)當(dāng)對(duì)賦 null 值的操作又過多的依賴瞧预,更沒有必要把它當(dāng)做一個(gè)普遍的編碼規(guī)則來推廣。原因有兩點(diǎn)仅政,從編碼角度講垢油,以恰當(dāng)?shù)淖兞孔饔糜騺砜刂谱兞炕厥諘r(shí)間才是最優(yōu)雅的解決方法。更關(guān)鍵的是圆丹,從執(zhí)行角度來將滩愁,使用賦 null 值的操作來優(yōu)化內(nèi)存回收是建立在對(duì)字節(jié)碼執(zhí)行引擎概念模型的理解之上的,而概念模型與實(shí)際執(zhí)行過程是外部看起來等效运褪,內(nèi)部看上去則可以完全不同惊楼。在虛擬機(jī)使用解釋器執(zhí)行時(shí)玖瘸,通常與概念模型還比較接近秸讹,但經(jīng)過 JIT 編譯器后,才是虛擬機(jī)執(zhí)行代碼的主要方式雅倒,賦 null 值的操作在經(jīng)過 JIT 編譯優(yōu)化后就會(huì)被消除掉璃诀,這時(shí)候?qū)⒆兞吭O(shè)置為 null 就是沒有意義的斗搞。字節(jié)碼被編譯為本地代碼后恼策,對(duì) GC Roots 的枚舉也與解釋執(zhí)行時(shí)期有巨大差別掸掸。
??????? 關(guān)于局部變量表湿弦,還有一點(diǎn)可能會(huì)對(duì)實(shí)際開發(fā)產(chǎn)生影響,就是局部變量不像前面介紹的類變量那樣存在 “準(zhǔn)備階段”凿将。通過之前的講解校套,我們已經(jīng)知道類變量有兩次賦初始值的過程,一次在準(zhǔn)備階段牧抵,賦予系統(tǒng)初始化笛匙;另外一次在初始化階段,賦予程序員定義的初始值犀变。因此妹孙,即使在初始化階段程序沒有為類變量賦值也沒有關(guān)系,類變量仍然具有一個(gè)確定的初始值获枝。但局部變量就不一樣蠢正,如果一個(gè)局部變量定義了但沒有賦初始值是不能使用的,不要認(rèn)為 Java 中任何情況下都存在諸如整型變量默認(rèn)為 0省店,布爾型變量默認(rèn)為 false 等這樣的默認(rèn)值嚣崭。如代碼清單 8-4 所示,這段代碼其實(shí)并不能運(yùn)行懦傍,還好編譯器能在編譯期間就檢查到并提示這一點(diǎn)有鹿。
操作數(shù)棧
????????操作數(shù)棧(Operand Stack)也常稱為操作棧,它是一個(gè)后入先出(Last In First Out谎脯,LIFO)棧葱跋。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候?qū)懭氲?Code 屬性的 max_stacks 數(shù)據(jù)項(xiàng)中源梭。操作數(shù)棧的每一個(gè)元素可以是任意的 Java 數(shù)據(jù)類型娱俺,包括 long 和 double。32 位數(shù)據(jù)類型所占的棧容量為 1,64 位數(shù)據(jù)類型所占的棧容量為 2废麻。在方法執(zhí)行的任何時(shí)候荠卷,操作數(shù)棧的深度都不會(huì)超過在 max_stacks 數(shù)據(jù)項(xiàng)中設(shè)定的最大值。
????????當(dāng)一個(gè)方法剛剛開始執(zhí)行的時(shí)候烛愧,這個(gè)方法的操作數(shù)棧是空的油宜,在方法的執(zhí)行過程中,會(huì)有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容怜姿,也就是出棧 / 入棧操作慎冤。例如,在做算術(shù)運(yùn)算的時(shí)候是通過操作數(shù)棧來進(jìn)行的沧卢,又或者再調(diào)用其他方法的時(shí)候是通過操作數(shù)棧來進(jìn)行參數(shù)傳遞的蚁堤。
??????? 舉個(gè)例子,整數(shù)加法的字節(jié)碼指令 iadd 在運(yùn)行的時(shí)候操作數(shù)棧中最接近棧頂?shù)膬蓚€(gè)元素已經(jīng)存入了兩個(gè) int 型的數(shù)值但狭,當(dāng)執(zhí)行這個(gè)指令時(shí)披诗,會(huì)將這兩個(gè) int 值出棧并相加撬即,然后將相加的結(jié)果入棧。
??????? 操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴(yán)格匹配呈队,在編譯程序代碼的時(shí)候剥槐,編譯器要嚴(yán)格保證這一點(diǎn),在類校驗(yàn)階段的數(shù)據(jù)流分析中還要再次驗(yàn)證這一點(diǎn)宪摧。再以上面的 iadd 指令為例才沧,這個(gè)指令用于整型數(shù)加法,它執(zhí)行時(shí)绍刮,最接近棧頂?shù)膬蓚€(gè)元素的數(shù)據(jù)類型必須為 int 型温圆,不能出現(xiàn)一個(gè) long 和一個(gè) float 使用 iadd 命令相加的情況。
??????? 另外孩革,在概念模型中岁歉,兩個(gè)棧幀作為虛擬機(jī)棧的元素,是完全相互獨(dú)立的膝蜈。但在大多虛擬機(jī)的實(shí)現(xiàn)里都會(huì)做一些優(yōu)化處理锅移,令兩個(gè)棧幀出現(xiàn)一部分重疊。讓下面棧幀的部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起饱搏,這樣在進(jìn)行方法調(diào)用時(shí)就可以共用一部分?jǐn)?shù)據(jù)非剃,無須進(jìn)行額外的參數(shù)復(fù)制傳遞,重疊的過程如圖 8-2 所示推沸。
????????Java 虛擬機(jī)的解釋執(zhí)行引擎稱為 “基于棧的執(zhí)行引擎”备绽,其中所指的 “棧” 就是操作數(shù)棧鬓催。
動(dòng)態(tài)連接
????????每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用肺素,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接(Dynamic Linking)。通過前面的講解宇驾,我們知道 Class 文件的常量池中存有大量的符號(hào)引用倍靡,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用作為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或者第一次使用的時(shí)候就轉(zhuǎn)化為直接引用课舍,這種轉(zhuǎn)化成為靜態(tài)解析塌西。另外一部分將在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,這部分稱為動(dòng)態(tài)連接筝尾。
方法返回地址
????????當(dāng)一個(gè)方法開始執(zhí)行后捡需,只有兩種方式可以退出這個(gè)方法。第一種方式是執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令忿等,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者(調(diào)用當(dāng)前方法的方法稱為調(diào)用者)栖忠,是否有返回值和返回值的類型將根據(jù)遇到何種方法返回指令來決定,這種退出方法的方式稱為正常完成出口(Normal Method Invocatino Completion)贸街。
????????另外一種退出方式是庵寞,在方法執(zhí)行過程中遇到了異常,并且這個(gè)異常沒有在方法體內(nèi)得到處理薛匪,無論是 Java 虛擬機(jī)內(nèi)部產(chǎn)生的異常捐川,還是代碼中使用 athrow 字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器逸尖,就會(huì)導(dǎo)致方法退出古沥,這種退出方法的方式稱為異常完成出口(Abrupt Method Invocation Completion)。一個(gè)方法使用異常完成出口的方式退出娇跟,是不會(huì)給它的上層調(diào)用者產(chǎn)生任何返回值的岩齿。
????????無論采用何種退出方式,在方法退出之后苞俘,都需要返回到方法被調(diào)用的位置盹沈,程序才能繼續(xù)執(zhí)行,方法返回時(shí)可能需要在棧幀中保存一些信息吃谣,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)乞封。一般來說,方法正常退出時(shí)岗憋,調(diào)用者的 PC 計(jì)數(shù)器的值可以作為返回地址肃晚,棧幀中很可能會(huì)保存這個(gè)計(jì)數(shù)器值。而方法異常退出時(shí)仔戈,返回地址是要通過異常處理器表來確定的关串,棧幀中一般不會(huì)保存這部分信息。
??????? 方法退出的過程實(shí)際上就等同于把當(dāng)前棧幀出棧监徘,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧悍缠,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整 PC 計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令等耐量。
附加信息
????????虛擬機(jī)規(guī)范允許具體的虛擬機(jī)實(shí)現(xiàn)增加一些規(guī)范里沒有描述的信息到棧幀之中飞蚓,例如與調(diào)試相關(guān)的信息,這部分信息完全取決于具體的虛擬機(jī)實(shí)現(xiàn)廊蜒。在實(shí)際開發(fā)中趴拧,一般會(huì)把動(dòng)態(tài)連接、方法返回地址與其他附加信息全部歸為一類山叮,稱為棧幀信息著榴。