聲明:本文摘抄自《深入理解Java虛擬機》一書,本文完全為自我學習,請感興趣的同學購買正版,支持原創
Java語言經常被人們定位為“解釋執行”語言,在Java初生的JDK1.0時代,這種定義還比較準確的,但當主流的虛擬機中都包含了即時編譯后,Class文件中的代碼到底會被解釋執行還是編譯執行,就成了只有虛擬機自己才能準確判斷的事情。再后來,Java也發展出來了直接生成本地代碼的編譯器[如何GCJ(GNU Compiler for the Java)],而C/C++也出現了通過解釋器執行的版本(如CINT),這時候再籠統的說“解釋執行”,對于整個Java語言來說就成了幾乎沒有任何意義的概念。
基于棧的指令集和基于寄存器的指令集
Java編譯器輸出的指令流,基本上是一種基于棧的指令集架構(Instruction Set Architecture,ISA),指令流中的指令大部分都是零地址指令,它們依賴操作數棧進行工作。與之相對應的另一套常用的指令集架構是基于寄存器的指令集,最典型的就是X86的地址指令集,說的通俗一下,就是現在我們主流的PC機中直接支持的指令集架構,這些指令集依賴寄存器工作。那么,基于棧的指令集和基于寄存器的指令集這兩者有什么不同呢?
舉個簡單例子,分別使用這兩種指令計算1+1的結果,基于棧的指令集會是這個樣子:
iconst_1
iconst_1
iadd
istore_0
兩條iconst_1指令連續把兩個常量1壓入棧后,iadd指令把棧頂的兩個值出棧、相加,然后將結果放回棧頂,最后istore_0把棧頂的值放到局部變量表中的第0個Slot中。
如果基于寄存器的指令集,那程序可能會是這個樣子:
mov eax, 1
add eax, 1
mov指令把EAX寄存器的值設置為1,然后add指令再把這個值加1,將結果就保存在EAX寄存器里面。
基于棧的指令集主要的優點就是可移植,寄存器是由硬件直接提供,程序直接依賴這些硬件寄存器則不可避免地要受到硬件的約束。例如,現在32位80x86體系的處理器中提供了8個32位的寄存器,而ARM體系的CPU則提供了16個32位的通用寄存器。如果使用棧架構的指令集,用戶程序不會直接使用寄存器,就可以由虛擬機實現來自行決定把一些訪問最頻繁的數據(程序計數器、棧頂緩存等)放到寄存器中以獲得最好的性能,這樣實現起來也更加簡單一些。棧架構的指令集還有一些其他的優點,如代碼相對更加緊湊,編譯器實現更加簡單等。
棧架構指令集的只要缺點是執行速度相對來說會稍微慢一些。雖然棧架構指令集的代碼非常緊湊,但是完成相同功能所需要的指令數量一般會比寄存器架構多,因為出棧、入棧操作本身就產生了相當多的操作指令數量。
更重要的是,棧實現在內存之中,頻繁的棧操作意味著頻繁的內存訪問,相對于處理器來說,內存始終是執行速度的瓶頸。盡管虛擬機采取棧頂緩存的優化手段,把最常用的操作映射到寄存器中避免直接內存訪問,但這也只能是優化措施而不是解決本質問題的方法。由于指令數量和內存訪問的原因,所以導致棧架構指令集的執行速度會相對較慢。
基于棧的解釋器的執行過程
public int calc() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
以上面的代碼為例,看看虛擬機是如何執行的。使用javap命令查看它的字節碼指令,字節碼指令如下:
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
}
編譯后的字節碼指令顯示這段代碼需要深度為2的操作數棧和4個Slot的局部變量空間。我們通過下面幾張圖來了解代碼執行過程中的代碼、操作數棧和局部變量表的變化情況。
- 首先執行偏移地址為0的指令,bipush指令的作用是將單個字節的整形常量值(-128~127)推入操作棧頂,跟隨有一個參數,指明推送的常量值,這里是100。
- 執行偏移地址為2的指令,istore_1指令的作用是將操作棧頂的整形值出棧并存入局部變量表Slot中。后續4條指令都是做一樣的事情,也就是在對應代碼中把變量a、b、c賦值為100、200、300。
- 執行偏移地址為11的指令,iload_1指令的作用是將局部變量表第一個Slot中的整形值復制到操作棧頂。
-
執行偏移地址為12的指令,iload_2指令的執行過程與iload_1類似,把第2個Slot的整形值入棧。當前局部變量表和操作棧如下圖所屬:
執行偏移地址為12的指令情況
5.執行偏移地址為13的指令,iadd指令的作用是將操作數棧中頭兩個棧頂元素出棧,做整形加法,然后把結果重新入棧。在iadd指令執行完畢后,棧中原有的100和200出棧,它們的和300重新入棧。
- 執行偏移地址為14的指令,iload_3指令把存放在第3個局部變量Slot中的300壓入操作棧中。這時操作棧中為兩個整數300。下一條指令imul是將操作棧中頭兩個棧頂元素出棧,做整形乘法,然后把結果重新入棧,與iadd完全類似。
- 執行偏移地址16的指令,ireturn指令是方法返回指令之一,它將結束方法執行并將操作棧頂的整形值返回給此方法的調用者。到此為止,這段代碼執行結束。