基于棧的字節(jié)碼解釋執(zhí)行引擎 深入理解Java虛擬機(jī)總結(jié)

? ???????虛擬機(jī)是如何調(diào)用方法的內(nèi)容已經(jīng)講解完畢,從本節(jié)開始,我們來探討虛擬機(jī)是如何執(zhí)行方法中的字節(jié)碼指令的。上文中提到過,許多 Java 虛擬機(jī)的執(zhí)行引擎在執(zhí)行 Java 代碼的時(shí)候都有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時(shí)編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇,在下面,我們先來探討一下在解釋執(zhí)行時(shí),虛擬機(jī)執(zhí)行引擎是如何工作的。

解釋執(zhí)行

??????? Java 語言經(jīng)常被人們定位為 “解釋執(zhí)行” 的語言,在 Java 初生的 JDK 1.0 時(shí)代,這種定義還算是比較準(zhǔn)確的,但當(dāng)主流的虛擬機(jī)中都包含了即時(shí)編譯器后Class 文件中的代碼到底會(huì)被解釋執(zhí)行還是編譯執(zhí)行,就成了只有虛擬機(jī)自己才能準(zhǔn)確判斷的事情。再后來,Java 也發(fā)展處了可以直接生成本地代碼的編譯器【如 GCJ(GNU Compiler for the Java)】,而 C/C++ 語言也出現(xiàn)了通過解釋器執(zhí)行的版本(如 CINT),這時(shí)候再籠統(tǒng)地說 “解釋執(zhí)行”,對(duì)于整個(gè) Java 語言來說就成了幾乎是沒有意義的概念,只有確定了談?wù)搶?duì)象是某種具體的 Java 實(shí)現(xiàn)版本和執(zhí)行引擎運(yùn)行模式時(shí),談解釋執(zhí)行還是貶義執(zhí)行才會(huì)比較確切。

??????? 不論是解釋還是編譯,也不論是物理機(jī)還是虛擬機(jī),對(duì)于應(yīng)用程序,機(jī)器都不可能如人那樣閱讀、理解,然后就獲得了執(zhí)行能力。大部分的程序代碼到物理機(jī)的目標(biāo)代碼或虛擬機(jī)能執(zhí)行的指令集之前,都需要經(jīng)過圖 8-4 中的各個(gè)步驟。如果讀者對(duì)編譯原理的相關(guān)課程還有印象的話,很容易就會(huì)發(fā)現(xiàn)圖 8-4 中下面那條分支,就是傳統(tǒng)編譯原理中程序代碼到目標(biāo)機(jī)器代碼的生成過程,而中間的那條分支,自然就是解釋執(zhí)行的過程。

????????如今,基于物理機(jī)、Java 虛擬機(jī),或者非 Java 的其他高級(jí)語言虛擬機(jī)(HLLVM)的語言,大多都會(huì)遵循這種基于現(xiàn)代經(jīng)典編譯原理的思路,在執(zhí)行前先對(duì)程序源碼進(jìn)行詞法分析和語法分析處理,把源碼轉(zhuǎn)化為抽象語法樹(Abstract Syntax Tree,AST)。對(duì)于一門具體語言的實(shí)現(xiàn)來說,詞法分析、語法分析以至于后面的優(yōu)化器和目標(biāo)代碼生成器都可以選擇獨(dú)立于執(zhí)行引擎,形成一個(gè)完整意義的編譯器去實(shí)現(xiàn),這類代表是 C/C++ 語言。也可以選擇把其中一部分步驟(如生成抽象語法樹之前的步驟)實(shí)現(xiàn)為一個(gè)半獨(dú)立的編譯器,這類代表是 Java 語言。又或者把這些步驟和執(zhí)行引擎全部集中封裝在一個(gè)封閉的黑匣子之中,如大多數(shù)的 JavaScript 執(zhí)行器。

??????? Java 語言中,Javac 編譯器完成了程序代碼經(jīng)過詞法分析、語法分析到抽象語法樹,再遍歷語法樹生成線性的字節(jié)碼指令流的過程。因?yàn)檫@一部分動(dòng)作是在 Java 虛擬機(jī)之外進(jìn)行的,而解釋器在虛擬機(jī)的內(nèi)部,所以 Java 程序的編譯就是半獨(dú)立的實(shí)現(xiàn)。



基于棧的指令集與基于寄存器的指令集

????????Java 編譯器輸出的指令流,基本上(注:使用 “基本上”,是因?yàn)椴糠肿止?jié)碼指令會(huì)帶有參數(shù),而純粹基于棧的指令集架構(gòu)中應(yīng)當(dāng)全部都是零地址指令,也即是都不存在顯式的參數(shù)。Java 這樣實(shí)現(xiàn)主要是考慮了代碼的可校驗(yàn)性)是一種基于棧的指令集架構(gòu)(Instruction Set Architecture, ISA),指令流中的指令大部分都是零地址指令,它們依賴操作數(shù)棧進(jìn)行工作。與之相對(duì)的另外一套常用的指令集架構(gòu)是基于寄存器的指令集,最典型的就是 x86 的二地址指令集,說得通俗一些,就是現(xiàn)在我們主流 PC 機(jī)中直接支持的指令集架構(gòu),這些指令依賴寄存器進(jìn)行工作。那么,基于棧的指令集與基于寄存器的指令集這兩者之間有什么不同呢?

舉個(gè)最簡單的例子,分別使用這兩種指令計(jì)算 “1+1” 的結(jié)果,基于棧的指令集會(huì)是這樣子的:

iconst_1??

iconst_1??

iadd??

istore_0??

??????? 兩條 iconst_1 指令連續(xù)把兩個(gè)常量 1 壓入棧后,iadd 指令把棧頂?shù)膬蓚€(gè)值出棧、相加,然后把結(jié)果放回棧頂,最后 istore_0 把棧頂?shù)闹捣诺骄植孔兞勘淼牡?0 個(gè) Slot 中。

??????? 如果基于寄存器,那程序可能會(huì)是這個(gè)樣子:

mov?eax,?1??

add?eax,1??

??????? mov 指令把 EAX 寄存器的值設(shè)為 1,然后 add 指令再把這個(gè)值加 1,結(jié)果就保存在 EAX 寄存器里面。

??????? 了解了基于棧的指令集與基于寄存器的指令集的區(qū)別后,讀者可能會(huì)有進(jìn)一步的疑問,這兩套指令集誰更好一些呢?應(yīng)該這么說,既然兩套指令集會(huì)同時(shí)并存和發(fā)展,那肯定是各有優(yōu)勢(shì)的,如果有一套指令集全面優(yōu)于另外一套的話,就不會(huì)存在選擇的問題了。

????????基于棧的指令集主要的優(yōu)點(diǎn)就是可移植,寄存器由硬件直接提供(注:這里說的是物理機(jī)器上的寄存器,也有基于寄存器的虛擬機(jī),如 Google Android 平臺(tái)的 Dalvik VM。即使是基于寄存器的虛擬機(jī),也希望把虛擬機(jī)寄存器盡量映射到物理寄存器上以獲取盡可能高的性能),程序直接依賴硬件寄存器則不可避免地要受到硬件的約束。例如,現(xiàn)在 32 位 80x86 體系的處理器中提供了 8 個(gè) 32 位的寄存器,而 ARM 體系的 CPU(在當(dāng)前的手機(jī)、PDA 中相當(dāng)流行的一種處理器)則提供了 16 個(gè) 32 位的通用寄存器。如果使用棧架構(gòu)的指令集,用戶程序不會(huì)直接使用這些寄存器,就可以由虛擬機(jī)實(shí)現(xiàn)來自行決定把一些訪問最頻繁的數(shù)據(jù)(程序計(jì)數(shù)器、棧頂緩存等)放到寄存器中以獲取盡量好的性能,這樣實(shí)現(xiàn)起來也更加簡單一些。棧架構(gòu)的指令集還有一些其他的有點(diǎn),如代碼相對(duì)更加緊湊(字節(jié)碼中每個(gè)字節(jié)就對(duì)應(yīng)一條指令,而多地址指令集中還需要存放參數(shù))、編譯器實(shí)現(xiàn)更加簡單(不需要考慮空間分配的問題,所需空間都在棧上操作)等。

? ??????棧架構(gòu)指令集的主要缺點(diǎn)是執(zhí)行速度相對(duì)來說會(huì)稍慢一些。所有主流物理機(jī)的指令集都是寄存器架構(gòu)也從側(cè)面印證了這一點(diǎn)。

??????? 雖然棧架構(gòu)指令集的代碼非常緊湊,但是完成相同功能所需的指令數(shù)量一般會(huì)比寄存器架構(gòu)多,因?yàn)槌鰲!⑷霔2僮鞅旧砭彤a(chǎn)生了相當(dāng)多的指令數(shù)量。更重要的是,棧實(shí)現(xiàn)在內(nèi)存之中,頻繁的棧訪問也就意味著頻繁的內(nèi)存訪問,相對(duì)于處理器來說,內(nèi)存始終是執(zhí)行速度的瓶頸。盡管虛擬機(jī)可以采取棧頂緩存的手段,把最常用的操作映射到寄存器中避免直接內(nèi)存訪問,但這也只能是優(yōu)化措施而不是解決本質(zhì)問題的方法。由于指令數(shù)量和內(nèi)存訪問的原因,所以導(dǎo)致了棧架構(gòu)指令集的執(zhí)行速度會(huì)相對(duì)較慢。


基于棧的解釋器執(zhí)行過程

?????????初步的理論知識(shí)已經(jīng)講解過了,本節(jié)準(zhǔn)備了一段 Java 代碼,看看在虛擬機(jī)中實(shí)際是如何執(zhí)行的。前面曾經(jīng)舉過一個(gè)計(jì)算 “1+1” 的例子,這樣的算術(shù)題目顯然太過簡單了,筆者準(zhǔn)備了四則運(yùn)算的例子,請(qǐng)看代碼清單 8-16。


?javap 提示這段代碼需要深度為 2 的操作數(shù)棧和 4 個(gè) Slot 局部變量空間,筆者根據(jù)這些信息畫了圖 8-5 ~ 圖 8-11 共 7 張圖,用它們來描述代碼清單 8-17 執(zhí)行過程中的代碼、操作數(shù)棧和局部變量表的變化情況。

??????? 上面的執(zhí)行過程僅僅是一種概念模型,虛擬機(jī)最終會(huì)對(duì)執(zhí)行過程做一些優(yōu)化來提高性能,實(shí)際的運(yùn)作過程不一定完全符合概念模型的描述……更準(zhǔn)確地說,實(shí)際情況會(huì)和上面描述的概念模型差距非常大,這種差距產(chǎn)生的原因是虛擬機(jī)中解析器和即時(shí)編譯器都會(huì)對(duì)輸入的字節(jié)碼進(jìn)行優(yōu)化,例如,在 HotSpot 虛擬機(jī)中,有很多以 “fast_” 開頭的非標(biāo)準(zhǔn)字節(jié)碼指令用于合并、替換輸入的字節(jié)碼以提升解釋執(zhí)行性能,而即時(shí)編譯器的優(yōu)化手段更加花樣繁多。

??????? 不過,我們從這段程序的執(zhí)行中也可以看出棧結(jié)構(gòu)指令集的一般運(yùn)行過程,整個(gè)運(yùn)算過程的中間變量都以操作數(shù)棧的出棧、入棧為信息交換途徑,符合我們?cè)谇懊娣治龅奶攸c(diǎn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容