0. 簡介
眾所周知,hotspot默認使用解釋+編譯混合
(-Xmixed)的方式執行代碼。它首先使用模板解釋器對字節碼進行解釋,當發現一段代碼是熱點的時候,就使用C1/C2 JIT進行優化編譯再執行,這也它的名字"熱點"(hotspot)的由來。
解釋器的代碼位於hotspot/share/interpreter
,它的總體架構如下:
1. 解釋器的兩種實現
首先hotspot有一個C++字節碼解釋器,還有一個模板解釋器 ,默認使用的是模板解釋器的實現。這兩個有什么區別呢?舉個例子,Java字節碼有istore_0
,iadd
,如果是C++字節碼解釋器(圖右部分),那么它的工作流程就是這種:
void cppInterpreter::work(){
for(int i=0;i<bytecode.length();i++){
switch(bytecode[i]){
case ISTORE_0:
int value = operandStack.pop();
localVar[0] = value;
break;
case IADD:
int v1 = operandStack.pop();
int v2 = operandStack.pop();
int res = v1+v2;
operandStack.push(res);
break;
....
}
}
}
它使用C++語言模擬字節碼的執行,iadd是兩個數相加,字節碼解釋器從棧上pop兩個數據然后求和,再push到棧上。
如果是模板解釋器就完全不一樣了。模板解釋器是一堆本地碼的例程(routines),它會在虛擬機創建的時候初始化好,也就是說,模板解釋器在初始化的時候會申請一片內存並設置為可讀可寫可執行,然后向那片內存寫入本地碼。在解釋執行的時候遇到iadd,就執行那片內存里面的二進制代碼。
這種運行時代碼生成的機制可以說是JIT,只是通常意義的JIT是指對一塊代碼進行優化再生成本地代碼,同一段代碼可能因為分成編譯產出不同的本地碼,具有動態性;而模板解釋器是虛擬機在創建的時候JIT生成它自身,它的每個例程比如異常處理部分,安全點處理部分的本地碼都是固定的,是靜態的。
2. 解釋器
2.1 抽象解釋器
再回到主題,架構圖有一個抽象解釋器,這個抽象解釋器描述了解釋器的基本骨架,它的屬性如下:
class AbstractInterpreter{
StubQueue* _code
address _slow_signature_handler;
address _entry_table[n];
address _cds_entry_table[n];
...
};
所有的解釋器(C++字節碼解釋器,模板解釋器)都有這些例程和屬性,然后子類的解釋器還可以再擴展一些例程。
我們重點關注_code
,它是一個隊列,
隊列中的InterpreterCodelet表示一個小例程,比如iconst_1對應的代碼,invokedynamic對應的代碼,異常處理對應的代碼,方法入口點對應的代碼,這些代碼都是一個個InterpreterCodelet...整個解釋器都是由這些小塊代碼例程組成的,每個小塊例程完成解釋器的部分功能,以此實現整個解釋器。
_entry_table
也是個重要的屬性,這個數組表示方法的例程,比如普通方法是入口點1_entry_table[0]
,帶synchronized的方法是入口點2_entry_table[1]
,這些_entry_table[0],_entry_table[1]
指向的就是之前_code隊列里面的小塊例程,就像這樣:
_entry_table[0] = _code->get_stub("iconst_1")->get_address();
_entry_table[1] = _code->get_stub("fconst_1")->get_address();
當然實際的實現遠比偽代碼復雜。
2.2 模板解釋器
前面說道小塊例程組合起來實現了解釋器,抽象解釋器定義了必要的例程,具體的解釋器在這之上還有自己的特設的例程。模板解釋器就是一個例子,它繼承自抽象解釋器,在那些例程之上還有自己的特設例程:
// 數組越界異常例程
static address _throw_ArrayIndexOutOfBoundsException_entry;
// 數組存儲異常例程
static address _throw_ArrayStoreException_entry;
// 算術異常例程
static address _throw_ArithmeticException_entry;
// 類型轉換異常例程
static address _throw_ClassCastException_entry;
// 空指針異常例程
static address _throw_NullPointerException_entry;
// 拋異常公共例程
static address _throw_exception_entry;
這樣做的好處是可以針對一些特殊例程進行特殊處理,同時還可以復用代碼。
到這里解釋器的布局應該是說清楚了,我們大概知道了:解釋器是一堆本地代碼例程構造的,這些例程會在虛擬機啟動的時候寫入,以后解釋就只需要進入指定例程即可。
3. 解釋器生成器
3.1 生成器與解釋器的關系
還有一個問題,這些例程是誰寫入的呢?找一找架構圖,下半部分都是解釋器生成器,它的名字也是自解釋的,那么它就是答案了。
前面刻意說道解釋器布局就是想突出它只是一個骨架,要得到可運行的解釋器還需要解釋器生成器填充這個骨架。
解釋器生成器本來可以獨自完成填充工作,可能為了解耦,也可能是為了結構清晰,hotspot將字節碼的例程抽了出來放到了templateTable(模板表)中,它輔助模板解釋器生成器(templateInterpreterGenerator)完成各例程填充。
只有這兩個還不能完成任務,因為組成模板解釋器的是本地代碼例程,本地代碼例程依賴於操作系統和CPU,這部分代碼位於hotspot/cpu/x86/
中,所以
templateInterpreter =
templateTable +
templateTable_x86 +
templateInterpreterGenerator +
templateInterpreterGenerator_x86 +
templateInterpreterGenerator_x86_64
虛擬機中有很多這樣的設計:在hotspot/share/
的某個頭文件寫出定義,在源文件實現OS/CPU無關的代碼,然后在hotspot/cpu/x86
中實現CPU相關的代碼,在hostpot/os
實現OS相關的代碼。
3.2 示例:數組越界異常例程生成
這么說可能有些蒼白無力,還是結合代碼更具說服力。
模板解釋器擴展了抽象解釋器,它有一個數組越界異常例程:
// 解釋器生成器
// hotspot\share\interpreter\templateInterpreterGenerator.cpp
void TemplateInterpreterGenerator::generate_all() {
...
{ CodeletMark cm(_masm, "throw exception entrypoints");
// 調用CPU相關的代碼生成例程
Interpreter::_throw_ArrayIndexOutOfBoundsException_entry = generate_ArrayIndexOutOfBounds_handler();
}
...
}
// 解釋器生成器中CPU相關的部分
// hotspot\os\x86\templateInterpreterGenerator_x86.cpp
address TemplateInterpreterGenerator::generate_ArrayIndexOutOfBounds_handler() {
address entry = __ pc();
__ empty_expression_stack();
// rarg是數組越界的對象,rbx是越界的索引
Register rarg = NOT_LP64(rax) LP64_ONLY(c_rarg1);
__ call_VM(noreg,
CAST_FROM_FN_PTR(address,
InterpreterRuntime::
throw_ArrayIndexOutOfBoundsException),
rarg, rbx);
return entry;
}
// 解釋器運行時
// hotspot\share\interpreter\interpreterRuntime.cpp
IRT_ENTRY(void, InterpreterRuntime::throw_ArrayIndexOutOfBoundsException(JavaThread* thread, arrayOopDesc* a, jint index))
ResourceMark rm(thread);
stringStream ss;
ss.print("Index %d out of bounds for length %d", index, a->length());
THROW_MSG(vmSymbols::java_lang_ArrayIndexOutOfBoundsException(), ss.as_string());
IRT_END
解釋器生成器會調用CPU相關的generate_ArrayIndexOutOfBounds_handler()生成異常處理例程,里面有個call_VM
,它調用了解釋器運行時(InterpreterRuntime)來處理異常。解釋器運行時是C++代碼,
之所以用它是因為異常處理比較麻煩,還需要C++其他模塊的支持(比如這里的stringStream和THROW_MSG),直接生成機器碼會非常麻煩,我們可以調用解釋器運行時相對輕松的處理。
我們在后面還會經常遇到call_VM
調用解釋器運行時這種模式,如果有很復雜的任務,需要其他C++模塊的支持,那么它就派上用場了。