天天看點

Runtime - 模闆解釋器

0. 簡介

衆所周知,hotspot預設使用

解釋+編譯混合

(-Xmixed)的方式執行代碼。它首先使用模闆解釋器對位元組碼進行解釋,當發現一段代碼是熱點的時候,就使用C1/C2 JIT進行優化編譯再執行,這也它的名字"熱點"(hotspot)的由來。

解釋器的代碼位于

hotspot/share/interpreter

,它的總體架構如下:

Runtime - 模闆解釋器

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

,它是一個隊列,

Runtime - 模闆解釋器

隊列中的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++子產品的支援,那麼它就派上用場了。

繼續閱讀