聲明:本文摘抄自《深入理解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的指令,bipush指令的作用是將單個字節(jié)的整形常量值(-128~127)推入操作棧頂,跟隨有一個參數(shù),指明推送的常量值,這里是100。
- 執(zhí)行偏移地址為2的指令,istore_1指令的作用是將操作棧頂?shù)恼沃党鰲2⒋嫒刖植孔兞勘鞸lot中。后續(xù)4條指令都是做一樣的事情,也就是在對應(yīng)代碼中把變量a、b、c賦值為100、200、300。
- 執(zhí)行偏移地址為11的指令,iload_1指令的作用是將局部變量表第一個Slot中的整形值復(fù)制到操作棧頂。
-
執(zhí)行偏移地址為12的指令,iload_2指令的執(zhí)行過程與iload_1類似,把第2個Slot的整形值入棧。當(dāng)前局部變量表和操作棧如下圖所屬:
執(zhí)行偏移地址為12的指令情況
5.執(zhí)行偏移地址為13的指令,iadd指令的作用是將操作數(shù)棧中頭兩個棧頂元素出棧,做整形加法,然后把結(jié)果重新入棧。在iadd指令執(zhí)行完畢后,棧中原有的100和200出棧,它們的和300重新入棧。
- 執(zhí)行偏移地址為14的指令,iload_3指令把存放在第3個局部變量Slot中的300壓入操作棧中。這時操作棧中為兩個整數(shù)300。下一條指令imul是將操作棧中頭兩個棧頂元素出棧,做整形乘法,然后把結(jié)果重新入棧,與iadd完全類似。
- 執(zhí)行偏移地址16的指令,ireturn指令是方法返回指令之一,它將結(jié)束方法執(zhí)行并將操作棧頂?shù)恼沃捣祷亟o此方法的調(diào)用者。到此為止,這段代碼執(zhí)行結(jié)束。
