線索解釋
一般來說,當提起解釋器的時候,大部分人腦海里面浮現出來的模型都是:
while(instruction != null){
switch (instruction) {
case ins1: doSomething1();
case ins2: doSomething2();
...
}
instruction = getNextInstruction();
}
其典型特征就是,在一個主循環里面使用switch來做指令的分派。這種解釋器模式被稱為譯碼分派(decode-and-dispatch),使用switch的情況下,也被稱為“switch-and-dispatch”。
這種實現方式雖然簡單直接,但是其本身在性能上表現十分糟糕。這種模式,最為影響性能的地方就在于指令分派那一步。指令分派需要線性遍歷每一條指令,最糟糕的情況下,就是要遍歷整個指令集??刂屏鞯霓D移也會對性能產生很大的影響。在現代處理器上,性能提高的一個很重要因素就是指令預測。然而在這種譯碼分派模式下,指令預測變得十分困難。
很顯然,HotSpot并沒有采用這種模式,它采用了一種被稱為線索解釋(Threaded interpretation)的方法。該方法對于譯碼分派有一個十分重大的改進:它將跳轉地址直接附在了指令之后。如圖:
這里的跳轉地址,在HotSpot使用模板解釋器的情況下,實際上是下一條字節碼指令對應的機器碼所在的地址。
模板解釋器
模板解釋器是一個很神奇的東西,它和一般人想的解釋是有很大的區別的。按照一般的想法,我們知道HotSpot是利用C++來實現的,那么相當然的就是以為對于每一條字節碼指令來說,其對應的解釋例程就是一段C++代碼。這也是對的,HotSpot早期的解釋器就是這樣實現的,這種解釋器被稱為字節碼解釋器。
但是用C++來解釋一條字節碼指令,肯定是很低效的??梢韵胍姷氖?,每一條字節碼的執行,都需要很長的一段C++代碼。舉個例子,add指令的C++實現方法,大概是先訪問兩次內存(也可以訪問一次,而后在分割),將操作數從操作數棧取出來,而后使用C++的加法操作符將其相加,然后再將結果寫進去操作數棧。整個過程,至少需要訪問兩次內存,還需要三個C++局部變量。
為了進一步提高性能,HotSpot使用了模板解釋器。模板解釋器概念上十分簡單,就是每一條字節碼指令都對應一個機器碼模板。這部分模板被放置在HotSpot的TemplateTable中:
// src/share/vm/interpreter/templateTable.cpp
void TemplateTable::initialize() {
//其余代碼,一些變量初始化
// interpr. templates
// Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument
def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _ );
def(Bytecodes::_aconst_null , ____|____|____|____, vtos, atos, aconst_null , _ );
def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 );
def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst , 0 );
//其余指令的定義
// 其余代碼
}
機器碼生成是在前面代碼中的generator那一列被指定的生成器完成的。舉例來說,在Bytecodes::_iconst_0的字節碼模板定義里面,指定的生成器叫做iconst。因為機器碼是依賴于具體的指令集架構的,所以這部分代碼放在:
// src/cpu/x86/vm/templateTable_x86_64.cpp
void TemplateTable::iconst(int value) {
transition(vtos, itos);
if (value == 0) {
__ xorl(rax, rax);
} else {
__ movl(rax, value);
}
}
機器碼生成
前面我們還提到,HotSpot是依賴于線索解釋的,也就是說,在當前字節碼指令對應的機器碼指令執行完成之后,應該跳轉到下一條字節碼指令對應的第一條機器碼指令的地址上。
要理解這一點,要先回到機器碼生成最開始的地方:
// src/share/vm/interpreter/templateInterpreter.cpp
void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) {
//其余代碼
// generate template
t->generate(_masm);
// advance
if (t->does_dispatch()) {
//...一些代碼
} else {
// dispatch to next bytecode
__ dispatch_epilog(tos_out, step);
}
}
最關鍵就是t->generate(_masm)和dispatch_epilog(tos_out, step)。第一句就是生成該字節碼對應的機器碼,參數_masm就是匯編生成器。
后面一句則是跳轉到了下一條字節碼指令那里。事實上,HotSpot第一步的確是找到下一條字節碼指令,這是通過將存儲現在字節碼地址的寄存器的值加上指令長度來實現的。但是HotSpot在執行機器碼指令的時候,執行完當前字節碼指令的最后一條機器碼指令之后,跳轉的是下一條字節碼指令對應的第一條機器碼指令地址。也就是意味著,在生成字節碼對應的機器碼之后,還要再生成跳轉的機器碼指令。
我們可以繼續追溯這個dispatch_epilog(tos_out, step)方法,直到:
// src/cpu/x86/vm/interp_masm_x86_64.cpp
void InterpreterMacroAssembler::dispatch_base(TosState state,
address* table,
bool verifyoop) {
// ...其余代碼
lea(rscratch1, ExternalAddress((address)table));
jmp(Address(rscratch1, rbx, Address::times_8));
}
jmp這一句,就是生成了這條跳轉的機器碼指令。
關于模板解釋器的源碼解讀,網上有很多的資源,讀者可以自行去查找,我這里就不重復前人已經做得很好的工作了