基于棧的字節(jié)碼解釋執(zhí)行引擎

聲明:本文摘抄自《深入理解Java虛擬機》一書,本文完全為自我學(xué)習(xí),請感興趣的同學(xué)購買正版,支持原創(chuàng)

Java語言經(jīng)常被人們定位為“解釋執(zhí)行”語言,在Java初生的JDK1.0時代,這種定義還比較準確的,但當(dāng)主流的虛擬機中都包含了即時編譯后,Class文件中的代碼到底會被解釋執(zhí)行還是編譯執(zhí)行,就成了只有虛擬機自己才能準確判斷的事情。再后來,Java也發(fā)展出來了直接生成本地代碼的編譯器[如何GCJ(GNU Compiler for the Java)],而C/C++也出現(xiàn)了通過解釋器執(zhí)行的版本(如CINT),這時候再籠統(tǒng)的說“解釋執(zhí)行”,對于整個Java語言來說就成了幾乎沒有任何意義的概念。

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

Java編譯器輸出的指令流,基本上是一種基于棧的指令集架構(gòu)(Instruction Set Architecture,ISA),指令流中的指令大部分都是零地址指令,它們依賴操作數(shù)棧進行工作。與之相對應(yīng)的另一套常用的指令集架構(gòu)是基于寄存器的指令集,最典型的就是X86的地址指令集,說的通俗一下,就是現(xiàn)在我們主流的PC機中直接支持的指令集架構(gòu),這些指令集依賴寄存器工作。那么,基于棧的指令集和基于寄存器的指令集這兩者有什么不同呢?

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

iconst_1
iconst_1
iadd
istore_0

兩條iconst_1指令連續(xù)把兩個常量1壓入棧后,iadd指令把棧頂?shù)膬蓚€值出棧、相加,然后將結(jié)果放回棧頂,最后istore_0把棧頂?shù)闹捣诺骄植孔兞勘碇械牡?個Slot中。
如果基于寄存器的指令集,那程序可能會是這個樣子:

mov eax, 1
add eax, 1

mov指令把EAX寄存器的值設(shè)置為1,然后add指令再把這個值加1,將結(jié)果就保存在EAX寄存器里面。
基于棧的指令集主要的優(yōu)點就是可移植,寄存器是由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免地要受到硬件的約束。例如,現(xiàn)在32位80x86體系的處理器中提供了8個32位的寄存器,而ARM體系的CPU則提供了16個32位的通用寄存器。如果使用棧架構(gòu)的指令集,用戶程序不會直接使用寄存器,就可以由虛擬機實現(xiàn)來自行決定把一些訪問最頻繁的數(shù)據(jù)(程序計數(shù)器、棧頂緩存等)放到寄存器中以獲得最好的性能,這樣實現(xiàn)起來也更加簡單一些。棧架構(gòu)的指令集還有一些其他的優(yōu)點,如代碼相對更加緊湊,編譯器實現(xiàn)更加簡單等。
棧架構(gòu)指令集的只要缺點是執(zhí)行速度相對來說會稍微慢一些。雖然棧架構(gòu)指令集的代碼非常緊湊,但是完成相同功能所需要的指令數(shù)量一般會比寄存器架構(gòu)多,因為出棧、入棧操作本身就產(chǎn)生了相當(dāng)多的操作指令數(shù)量。
更重要的是,棧實現(xiàn)在內(nèi)存之中,頻繁的棧操作意味著頻繁的內(nèi)存訪問,相對于處理器來說,內(nèi)存始終是執(zhí)行速度的瓶頸。盡管虛擬機采取棧頂緩存的優(yōu)化手段,把最常用的操作映射到寄存器中避免直接內(nèi)存訪問,但這也只能是優(yōu)化措施而不是解決本質(zhì)問題的方法。由于指令數(shù)量和內(nèi)存訪問的原因,所以導(dǎo)致棧架構(gòu)指令集的執(zhí)行速度會相對較慢。

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

public int calc() {
    int a = 100;
    int b = 200;
    int c = 300;
    return (a + b) * c;
}

以上面的代碼為例,看看虛擬機是如何執(zhí)行的。使用javap命令查看它的字節(jié)碼指令,字節(jié)碼指令如下:

  public int calc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100
         2: istore_1
         3: sipush        200
         6: istore_2
         7: sipush        300
        10: istore_3
        11: iload_1
        12: iload_2
        13: iadd
        14: iload_3
        15: imul
        16: ireturn
      LineNumberTable:
        line 3: 0
        line 4: 3
        line 5: 7
        line 6: 11
}

編譯后的字節(jié)碼指令顯示這段代碼需要深度為2的操作數(shù)棧和4個Slot的局部變量空間。我們通過下面幾張圖來了解代碼執(zhí)行過程中的代碼、操作數(shù)棧和局部變量表的變化情況。


執(zhí)行偏移地址為0的指令情況
  1. 首先執(zhí)行偏移地址為0的指令,bipush指令的作用是將單個字節(jié)的整形常量值(-128~127)推入操作棧頂,跟隨有一個參數(shù),指明推送的常量值,這里是100。
  2. 執(zhí)行偏移地址為2的指令,istore_1指令的作用是將操作棧頂?shù)恼沃党鰲2⒋嫒刖植孔兞勘鞸lot中。后續(xù)4條指令都是做一樣的事情,也就是在對應(yīng)代碼中把變量a、b、c賦值為100、200、300。
  3. 執(zhí)行偏移地址為11的指令,iload_1指令的作用是將局部變量表第一個Slot中的整形值復(fù)制到操作棧頂。
  4. 執(zhí)行偏移地址為12的指令,iload_2指令的執(zhí)行過程與iload_1類似,把第2個Slot的整形值入棧。當(dāng)前局部變量表和操作棧如下圖所屬:


    執(zhí)行偏移地址為12的指令情況

    5.執(zhí)行偏移地址為13的指令,iadd指令的作用是將操作數(shù)棧中頭兩個棧頂元素出棧,做整形加法,然后把結(jié)果重新入棧。在iadd指令執(zhí)行完畢后,棧中原有的100和200出棧,它們的和300重新入棧。

  5. 執(zhí)行偏移地址為14的指令,iload_3指令把存放在第3個局部變量Slot中的300壓入操作棧中。這時操作棧中為兩個整數(shù)300。下一條指令imul是將操作棧中頭兩個棧頂元素出棧,做整形乘法,然后把結(jié)果重新入棧,與iadd完全類似。
  6. 執(zhí)行偏移地址16的指令,ireturn指令是方法返回指令之一,它將結(jié)束方法執(zhí)行并將操作棧頂?shù)恼沃捣祷亟o此方法的調(diào)用者。到此為止,這段代碼執(zhí)行結(jié)束。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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