­

第62篇-解釋器與編譯器適配(一)

  • 2022 年 2 月 11 日
  • 筆記

對棧上替換的nmethod而言,執行棧上替換就相當於安裝,因為棧上替換的nmethod都是方法內部的調用,所以實現相對簡單點。對非棧上替換的nmethod而言,其安裝稍微複雜點,需要考慮從Java程式碼和本地程式碼中調用nmethod安裝完成的方法的情形,HotSpot VM的實現是通過一個在位元組碼解釋執行的棧幀和本地程式碼執行的棧幀之間做切換適配的適配器來完成安裝,適配器和位元組碼指令一樣都是通過彙編實現的。

下面看一下解釋執行與編譯執行的入口,調用的函數link_method()的實現如下:

void Method::link_method(methodHandle h_method, TRAPS) {
  if (_i2i_entry != NULL){
	  return;
  }
  // 設置_i2i_entry和_from_interpreted_entry屬性的值
  address entry = Interpreter::entry_for_method(h_method);
  set_interpreter_entry(entry);

  // 設置Method::_from_compiler_entry和Method::_adapter屬性的值
  (void) make_adapters(h_method, CHECK);
}

方法鏈接主要就是做的事就是設置 Method::_from_interpreter_entry,過程主要是根據方法類型,獲取並保存方法對應的入口常式的地址。  

《深入剖析Java虛擬機:源碼剖析與實例詳解(基礎卷)》一書中詳細介紹了方法Method及其中定義的重要屬性,如_i2i_entry 與 _from_interpreted_entry等,這裡不再介紹。set_interpreter_entry()函數設置了_i2i_entry 與 _from_interpreted_entry這2個屬性的值,如下:

源程式碼位置:openjdk/hotspot/src/share/vm/oops/method.hpp文件

void set_interpreter_entry(address entry){
 _i2i_entry = entry;
 _from_interpreted_entry = entry;
}

_i2i_entry指向方法的解釋器入口,此值設置後不會再改變。在方法連接時,_from_interpreted_entry和_i2i_entry指向的都是之前詳細介紹過的、調用generate_call_stub()函數生成的解釋執行的入口常式。

_from_interpreted_entry 初始的值與 _i2i_entry 一樣,但後面當該Java方法被JIT編譯並「安裝」之後,_from_interpreted_entry 就會被設置為指向 i2c adapter stub(設置邏輯在前一篇介紹的Method::set_code()函數中)。而如果因為某些原因需要拋棄掉之前已經編譯並安裝好的機器碼,則 _from_interpreted_entry 會被恢復為 _i2i_entry(恢復邏輯在前一篇介紹的Method::clear_code()函數中)。如下圖所示。

  

如上的Method::link_method()函數中調用的make_adapters()函數的實現如下:

address Method::make_adapters(methodHandle mh, TRAPS) {
  AdapterHandlerEntry* adapter = AdapterHandlerLibrary::get_adapter(mh);

  mh->set_adapter_entry(adapter);
  mh->_from_compiled_entry = adapter->get_c2i_entry();
  return adapter->get_c2i_entry();
}

Method::_adapter屬性就是一個AdapterHandlerEntry指針,AdapterHandlerEntry表示一個棧幀轉換的適配器,因為位元組碼解釋執行時的棧幀結構和暫存器的使用與編譯後的本地程式碼執行時的完全不同,需要在位元組碼解釋執行和本地程式碼執行兩者之間切換時對棧幀和暫存器等做必要的轉換處理。允許從位元組碼解釋執行切換到本地程式碼執行(即I2C)以及從本地程式碼執行切換到位元組碼解釋執行(即C2I)。AdapterHandlerEntry本身很簡單,只是一個保存I2C和C2I適配器地址的容器而已,此類及重要屬性的定義如下:

class AdapterHandlerEntry : public BasicHashtableEntry<mtCode> {
 private:
  // 方法簽名相同時,可以重用適配器常式,所以_fingerprint代表著某個方法的簽名
  AdapterFingerPrint* _fingerprint;
  // 解釋執行切換到編譯執行的適配器,通過調用_adapter->i2c_entry()函數獲取
  address  _i2c_entry; 
  // 編譯執行切換到解釋執行的適配器,通過調用_adapter->c2i_entry()函數獲取
  address  _c2i_entry; 
  // 編譯執行切換到解釋執行的適配器,通過調用_adapter->get_c2i_unverified_entry()函數獲取
  address  _c2i_unverified_entry; 
  // ...
}

 AdapterHandlerLibrary是一個用來生成AdapterHandlerEntry的一個工具類,其中定義的get_apapter()函數非常重要,會創建AdapterHandlerEntry實例並初始化其中的_i2c_entry、_c2i_entry等屬性。調用get_c2i_entry()函數獲取_c2i_entry屬性的值並初始化_from_compiled_entry屬性,如下圖所示。

從adapter中調用get_i2c_entry()或get_c2i_entry()函數就可以在解釋執行和編譯執行之間進行適配,因為解釋執行和編譯執行的調用約定(calling convention)不同,所以要進行適配。

_from_compiled_entry被初始化為指向c2i adapter stub(方法連接時調用Method::make_adapters()函數設置)),因為方法在開始的時候並沒有被JIT編譯,只能解釋執行。如果從已編譯的Java方法調用過來的話就需要適配調用約定。當方法被JIT編譯並「安裝」完之後,_from_compiled_entry會指向編譯出來的機器碼的入口,具體說就是指向verified entry point(設置邏輯在前一篇介紹的Method::set_code()函數中)。如果要拋棄之前編譯好的機器碼,那麼 _from_compiled_entry 會恢復指向為c2i adapter stub(恢復邏輯在前一篇介紹的Method::clear_code()函數中)。

當編譯完成後,會對編譯完成的方法生成一個nmethod實例。nmethod類的全名native method,指向的是Java方法編譯的一個版本。通過Method::_code屬性來存儲,如下:

nmethod* volatile  _code;

Method::_code指向JIT編譯後的機器碼。初始值為NULL,意味著該方法尚未被JIT編譯。當一個方法被JIT編譯並「安裝」後,_code 就會指向編譯生成的 nmethod,而要拋棄編譯好的程式碼時,_code屬性重新設置為NULL即可。實際上,在nmethod類中也定義了2個編譯方法的入口,由如下2個屬性保存:

// 需要進行類檢測的入口
address           _entry_point;           
// 沒有類檢查的入口
address           _verified_entry_point;  

每個Method有兩個實際入口,一個是unverified entry point(UEP),用於實現虛方法分派的monomorphic inline cache;另一個是verified entry point(VEP),是方法的真正入口。只有需要虛方法分派的方法才會有獨立的UEP;對靜態方法、私有成員方法之類的Java方法,UEP與VEP實際上在同一個位置,後面還會詳細介紹UEP與VEP 。 

在Method::make_adapters()函數中調用AdapterHandlerLibrary::get_adapter()函數獲取AdapterHandlerEntry,這樣就能獲取到i2c、c2i等stub的入口地址了。下面看一下get_adapter()函數的實現,如下:

// 傳遞了method參數,所以是針對特定方法來實現的
AdapterHandlerEntry* AdapterHandlerLibrary::get_adapter(methodHandle method) {
  address ic_miss = SharedRuntime::get_ic_miss_stub();

  ResourceMark rm;

  AdapterBlob*          B = NULL;
  AdapterHandlerEntry*  entry = NULL;
  AdapterFingerPrint*   fingerprint = NULL;

  ///////////////////////////////////////////////////////////////////////////////////////
  {
    MutexLocker mu(AdapterHandlerLibrary_lock);
    // 主要初始化AdapterHandlerLibrary::_adapters屬性
    initialize(); 

    if (method->is_abstract()) {
       return _abstract_method_handler;
    }


    int total_args_passed = method->size_of_parameters(); 

    // 宏擴展後為:(BasicType*) resource_allocate_bytes(  (total_args_passed) * sizeof(BasicType)  )
    // BasicType為枚舉類型,在之前介紹調用約定時詳細介紹過
    BasicType* sig_bt = NEW_RESOURCE_ARRAY(BasicType, total_args_passed);
    // 宏擴展後為:(VMRegPair*) resource_allocate_bytes(  (total_args_passed) * sizeof(VMRegPair)  )
    VMRegPair* regs   = NEW_RESOURCE_ARRAY(VMRegPair, total_args_passed);

    int i = 0;
    if (!method->is_static()){  
      sig_bt[i++] = T_OBJECT; // 實例方法傳遞的第一個參數為this指針
    }

    for (SignatureStream ss(method->signature()); !ss.at_return_type(); ss.next()) {
      sig_bt[i++] = ss.type();  
      if (ss.type() == T_LONG || ss.type() == T_DOUBLE){
        sig_bt[i++] = T_VOID;   // 對於Long和Double類型來說,佔用2個slot
      }
    }
    assert(i == total_args_passed, "");
    // 針對不同的方法簽名生成不同的AdapterHandlerEntry,所以需要一個AdapterHandlerTable容器來存儲
    // 這樣,相同的方法簽名就可以重用AdapterHandlerEntry,如果查找了,直接返回就可以
    entry = _adapters->lookup(total_args_passed, sig_bt);
    if (entry != NULL) {
      return entry;
    }

    // 根據java編譯執行的調用約定計算出需要通過棧來傳遞的參數的棧大小
    int comp_args_on_stack = SharedRuntime::java_calling_convention(sig_bt, regs, total_args_passed, false);

    fingerprint = new AdapterFingerPrint(total_args_passed, sig_bt);

    // 為AdapterHandlerEntry中的_i2c_entry、_c2i_entry等生成對應的stub

    BufferBlob* buf = buffer_blob(); 
    if (buf != NULL) {
      CodeBuffer      buffer(buf);
      short           buffer_locs[20];
      CodeSection*    csn = buffer.insts();
      csn->initialize_shared_locs( (relocInfo*)buffer_locs, sizeof(buffer_locs)/sizeof(relocInfo) );
      MacroAssembler  _masm(&buffer);

      entry = SharedRuntime::generate_i2c2i_adapters(
								 &_masm,
								 total_args_passed,
								 comp_args_on_stack,
								 sig_bt,
								 regs,
								 fingerprint
							  );

      B = AdapterBlob::create(&buffer); // 創建了AdapterBlob對象
    } // 結束  buf != NULL


    address ctmp = B->content_begin();
    entry->relocate(ctmp);

    _adapters->add(entry);
  }
  //////////////////////////////////////////////////////////////////////////////////////////

  return entry;
}

這裡我們需要介紹3個知識點:

第1個就是AdapterHandlerLibrary::_adapters屬性,類型為AdapterHandlerTable*,這是一個容器,存儲著許多AdapterHandlerEntry,如果容器的同一個位置有多個AdapterHandlerEntry,則使用單鏈表連接起來,而AdapterHandlerEntry中定義了如下3個屬性:

AdapterFingerPrint* _fingerprint;
address _i2c_entry;
address _c2i_entry;
address _c2i_unverified_entry;

其中的第1個屬性是為了重用i2c stub、c2i stub的,因為方法參數不同,每次需要生成的stub也不同,但是如果方法相同,這些stub是可以重用的,所以使用AdapterHandlerTable來重用stub。每次在生成stub之前,查看一下AdapterHandlerTable是否有可重用的stub,如果沒有就需要生成。

第2個需要介紹的就是生成AdapterBlob,相關的機器程式碼首先會生成到CodeBuffer中,最終會通過AdapterBlob來存儲,之前為位元組碼生成機器碼時,也是首先生成到CodeBuffer中,然後通過BufferBlob來存儲。之所以要這樣做,就是為了在記憶體中將生成的各個Stub放的整齊一些,一旦放整齊了就不會再移動,除非被卸載。

第3個知識點最重要,調用SharedRuntime::generate_i2c2i_adapters()函數生成i2c stub和c2i stub,這個函數共生成3個機器片段,其入口用AdapterHandlerEntry類中的如上3個屬性保存。下一篇將詳細介紹。

參考文章:

(1)HotSpot中執行引擎技術詳解(一)——分階段程式碼執行 

(2)//hllvm-group.iteye.com/group/topic/37707

 公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源程式碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流