深入底層|JVM原始碼解讀:HotSpot的模板直譯器

HeapDump效能社群發表於2021-11-18
本文由HeapDump效能社群首席講師鳩摩(馬智)授權整理髮布

第9篇-位元組碼指令的定義

之前的文章介紹瞭解釋執行下的Java棧幀建立以及位元組碼分派邏輯,但是始終沒有講到虛擬機器到底是怎麼執行Java方法中的位元組碼的,在介紹位元組碼的執行之前,需要先知道位元組碼指令的定義。在Bytecodes::initialize()函式中會定義位元組碼指令的一些屬性。這個函式的呼叫鏈如下:

init_globals()
bytecodes_init() 
Bytecodes::initialize()

在Bytecodes::initialize()函式中有類似這樣的定義:

//  bytecode               bytecode name           format   wide f.   result tp  stk traps
def(_nop                 , "nop"                 , "b"    , NULL    , T_VOID   ,  0, false);
def(_aconst_null         , "aconst_null"         , "b"    , NULL    , T_OBJECT ,  1, false);
def(_iconst_m1           , "iconst_m1"           , "b"    , NULL    , T_INT    ,  1, false);
def(_iconst_0            , "iconst_0"            , "b"    , NULL    , T_INT    ,  1, false);
def(_iconst_1            , "iconst_1"            , "b"    , NULL    , T_INT    ,  1, false);
// ...

現在Java虛擬機器規範定義的202個位元組碼指令都會向上圖那樣,呼叫def()函式進行定義,我們需要重點關注呼叫def()函式時傳遞的引數bytecode name、format等。 下面一個一個解釋,如下:

  • bytecode name就是位元組碼名稱;
  • wide表示位元組碼前面是否可以加wide,如果可以,則值為"wbii";
  • result tp表示指令執行後的結果型別,如為T_ILLEGAL時,表示只參考當前位元組碼無法決定執行結果的型別,如_invokevirtual方法呼叫指令,結果型別應該為方法返回型別,但是此時只參考這個呼叫方法的位元組碼指令是無法決定的;
  • stk表示對錶達式棧深度的影響,如_nop指令不執行任何操作,所以對錶達式棧的深度無影響,stk的值為0;當用_iconst_0向棧中壓入0時,棧的深度增加1,所以stk的值為1。當為_lconst_0時,棧的深度會增加2;當為_lstore_0時,棧的深度會減少2;
  • traps表示can_trap,這個比較重要,在後面會詳細介紹。
  • format,這個屬效能表達2個意思,首先能表達位元組碼的格式,另外還能表示位元組碼的長度。

下面我們需要重點介紹一下format這個引數。format表示位元組碼的格式,當字串中有一個字元時就是一個位元組長度的位元組碼,當為2個字元時就是2個位元組長度的位元組碼...,如_iconst_0就是一個位元組寬度的位元組碼,_istore的format為"bi",所以是2個位元組寬度。format還可能為空字串,當為空字串時,表示當前的位元組碼不是Java虛擬機器規範中定義的位元組碼,如為了提高解釋執行效率的_fast_agetfield、_fast_bgetfield等位元組碼,這些位元組碼是虛擬機器內部定義的。還能表達位元組碼的格式,其中的字串中各個字元的含義如下:

b: 表示位元組碼指令是非可變長度的,所以對於tableswitch、lookupswitch這種可變長度的指令來說,format字串中不會含有b字元;
c:運算元為有符號的常量,如bipush指令將byte帶符號擴充套件為一個int型別的值,然後將這個值入棧到運算元棧中;
i:運算元為無符號的本地變數表索引值,如iload指令從區域性變數表載入一個int型別的值到運算元棧中;
j:運算元為常量池快取的索引,注意常量池快取索引不同與常量池索引,關於常量池索引,在《深入剖析Java虛擬機器:原始碼剖析與例項詳解》基礎卷中詳細介紹過,這裡不再介紹;
k:運算元為無符號的常量池索引,如ldc指令將從執行時常量池中提取資料並壓入運算元棧,所以格式為"bk";
o:運算元為分支偏移,如ifeq表示整數與零比較,如果整數為0,則比較結果為真,將運算元看為分支偏移量進行跳轉,所以格式為”boo“;
_:可直接忽略
w:可用來擴充套件區域性變數表索引的位元組碼,這些位元組碼有iload、fload等,所以wild的值為"wbii";

呼叫的def()函式的實現如下:

void Bytecodes::def(
Code          code,
const char*   name,
const char*   format,
const char*   wide_format,
BasicType     result_type,
int           depth,
bool          can_trap,
Code          java_code
) {
  int len  = (format      != NULL ? (int) strlen(format)      : 0);
  int wlen = (wide_format != NULL ? (int) strlen(wide_format) : 0);

  _name          [code] = name;
  _result_type   [code] = result_type;
  _depth         [code] = depth;
  _lengths       [code] = (wlen << 4) | (len & 0xF); // 0xF的二進位制值為1111
  _java_code     [code] = java_code;


  int bc_flags = 0;
  if (can_trap){
    // ldc、ldc_w、ldc2_w、_aload_0、iaload、iastore、idiv、ldiv、ireturn等
    // 位元組碼指令都會含有_bc_can_trap
    bc_flags |= _bc_can_trap; 
  }
  if (java_code != code){
    bc_flags |= _bc_can_rewrite; // 虛擬機器內部定義的指令都會有_bc_can_rewrite
  }

  // 在這裡對_flags賦值操作
  _flags[(u1)code+0*(1<<BitsPerByte)] = compute_flags(format,      bc_flags);
  _flags[(u1)code+1*(1<<BitsPerByte)] = compute_flags(wide_format, bc_flags);
}

其中的_name、_result_type等都是在Bytecodes類中定義的靜態陣列,其下標為Opcode值,而儲存的值就是name、result_type等。這些變數的定義如下:

const char*     Bytecodes::_name          [Bytecodes::number_of_codes];
BasicType       Bytecodes::_result_type   [Bytecodes::number_of_codes];
s_char          Bytecodes::_depth         [Bytecodes::number_of_codes];
u_char          Bytecodes::_lengths       [Bytecodes::number_of_codes];
Bytecodes::Code Bytecodes::_java_code     [Bytecodes::number_of_codes];
u_short         Bytecodes::_flags         [(1<<BitsPerByte)*2];

Bytecodes::number_of_codes的值為234,足夠儲存所有的位元組碼指令了(包含虛擬機器內部擴充套件的指令)。 

回看Bytecodes::def()函式,通過呼叫compute_flags()函式根據傳入的wide_format和format來計算位元組碼的一些屬性,然後儲存到高8位和低8位中。呼叫的compute_flags()函式的實現如下:

int Bytecodes::compute_flags(const char* format, int more_flags) {
  if (format == NULL) {
      return 0;  // not even more_flags
  }

  int flags = more_flags;
  const char* fp = format;
  switch (*fp) {
  case '\0':
    flags |= _fmt_not_simple; // but variable
    break;
  case 'b':
    flags |= _fmt_not_variable;  // but simple
    ++fp;  // skip 'b'
    break;
  case 'w':
    flags |= _fmt_not_variable | _fmt_not_simple;
    ++fp;  // skip 'w'
    guarantee(*fp == 'b', "wide format must start with 'wb'");
    ++fp;  // skip 'b'
    break;
  }

  int has_nbo = 0, has_jbo = 0, has_size = 0;
  for (;;) {
    int this_flag = 0;
    char fc = *fp++;
    switch (fc) {
    case '\0':  // end of string
      assert(flags == (jchar)flags, "change _format_flags");
      return flags;

    case '_': continue;         // ignore these

    case 'j': this_flag = _fmt_has_j; has_jbo = 1; break;
    case 'k': this_flag = _fmt_has_k; has_jbo = 1; break;
    case 'i': this_flag = _fmt_has_i; has_jbo = 1; break;
    case 'c': this_flag = _fmt_has_c; has_jbo = 1; break;
    case 'o': this_flag = _fmt_has_o; has_jbo = 1; break;

    case 'J': this_flag = _fmt_has_j; has_nbo = 1; break;
    ...
    default:  guarantee(false, "bad char in format");
    }// 結束switch

    flags |= this_flag;

    guarantee(!(has_jbo && has_nbo), "mixed byte orders in format");
    if (has_nbo){
      flags |= _fmt_has_nbo;
    }

    int this_size = 1;
    if (*fp == fc) {
      // advance beyond run of the same characters
      this_size = 2;
      while (*++fp == fc){
          this_size++;
      }
      switch (this_size) {
      case 2: flags |= _fmt_has_u2; break; // 如sipush、ldc_w、ldc2_w、wide iload等
      case 4: flags |= _fmt_has_u4; break; // 如goto_w和invokedynamic指令
      default:
          guarantee(false, "bad rep count in format");
      }
    }

    has_size = this_size;
  }
}

函式要根據wide_format和format來計算flags的值,通過flags中的值能夠表示位元組碼的b、c、i、j、k、o、w(在之前介紹format時介紹過)和位元組碼運算元的大小(運算元是2位元組還是4位元組)。以_fmt開頭的一些變數在列舉類中已經定義,如下:

// Flag bits derived from format strings, can_trap, can_rewrite, etc.:
enum Flags {
// semantic flags:
_bc_can_trap      = 1<<0,     // bytecode execution can trap(卡住) or block
// 虛擬機器內部定義的位元組碼指令都會含有這個標識
_bc_can_rewrite   = 1<<1,     // bytecode execution has an alternate(代替者) form

// format bits (determined only by the format string):
_fmt_has_c        = 1<<2,     // constant, such as sipush "bcc"
_fmt_has_j        = 1<<3,     // constant pool cache index, such as getfield "bjj"
_fmt_has_k        = 1<<4,     // constant pool index, such as ldc "bk"
_fmt_has_i        = 1<<5,     // local index, such as iload
_fmt_has_o        = 1<<6,     // offset, such as ifeq

_fmt_has_nbo      = 1<<7,     // contains native-order field(s)
_fmt_has_u2       = 1<<8,     // contains double-byte field(s)
_fmt_has_u4       = 1<<9,     // contains quad-byte field
_fmt_not_variable = 1<<10,    // not of variable length (simple or wide) 不可變長度的指令
_fmt_not_simple   = 1<<11,    // either wide or variable length 或者是可加wild的位元組碼指令,或者是可變長度的指令
_all_fmt_bits     = (_fmt_not_simple*2 - _fmt_has_c),

// ...
};

與format的對應關係如下: 

這樣通過組合就可表示出不同的值,列舉類中定義了常用的組合如下:

_fmt_b      = _fmt_not_variable,
_fmt_bc     = _fmt_b | _fmt_has_c,
_fmt_bi     = _fmt_b | _fmt_has_i,
_fmt_bkk    = _fmt_b | _fmt_has_k | _fmt_has_u2,
_fmt_bJJ    = _fmt_b | _fmt_has_j | _fmt_has_u2 | _fmt_has_nbo,
_fmt_bo2    = _fmt_b | _fmt_has_o | _fmt_has_u2,
_fmt_bo4    = _fmt_b | _fmt_has_o | _fmt_has_u4

例如位元組碼為bipush時,format就是"bc",那麼flags的值為_fmt_b | _fmt_has_c,ldc位元組碼的format為"bk",則flags的值為_fmt_b | _fmt_has_k。 


第10篇-初始化模板表

第9篇-位元組碼指令的定義我們介紹了位元組碼指令並且將位元組碼指令相關的資訊都儲存到了相關陣列中,只需要通過Opcode就可從相關陣列中獲取對應的資訊。

在init_globals()函式中呼叫bytecodes_init()函式初始化好位元組碼指令後會呼叫interpreter_init()函式初始化直譯器。函式最終會呼叫到TemplateInterpreter::initialize()函式。這個函式的實現如下:

原始碼位置:/src/share/vm/interpreter/templateInterpreter.cpp

void TemplateInterpreter::initialize() {
  if (_code != NULL) 
       return;
   
  // 抽象直譯器AbstractInterpreter的初始化,
  // AbstractInterpreter是基於彙編模型的直譯器的共同基類,
  // 定義瞭直譯器和直譯器生成器的抽象介面
  AbstractInterpreter::initialize();
   
  // 模板表TemplateTable的初始化,模板表TemplateTable儲存了各個位元組碼的模板
  TemplateTable::initialize();
   
  // generate interpreter
  {
     ResourceMark rm;
     int code_size = InterpreterCodeSize;
     // CodeCache的Stub佇列StubQueue的初始化
     _code = new StubQueue(new InterpreterCodeletInterface, code_size, NULL,"Interpreter");
     //  例項化模板直譯器生成器物件TemplateInterpreterGenerator
     InterpreterGenerator g(_code);
  }
   
  // 初始化位元組分發表
  _active_table = _normal_table;
}

這個初始化函式中涉及到的初始化邏輯比較多,而且比較複雜。我們將初始化分為4部分:

(1)抽象直譯器AbstractInterpreter的初始化,AbstractInterpreter是基於彙編模型的直譯器的共同基類,定義瞭直譯器和直譯器生成器的抽象介面。
(2)模板表TemplateTable的初始化,模板表TemplateTable儲存了各個位元組碼的模板(目的碼生成函式和引數);
(3)CodeCache的Stub佇列StubQueue的初始化;
(4)直譯器生成器InterpreterGenerator的初始化。

其中抽象直譯器初始化時會涉及到一些計數,這些計數主要與編譯執行有關,所以這裡暫不過多介紹,到後面介紹編譯執行時再介紹。

下面我們分別介紹如上3個部分的初始化過程,這一篇只介紹模板表的初始化過程。

函式TemplateTable::initialize()的實現如下:

模板表TemplateTable儲存了各個位元組碼的執行模板(目的碼生成函式和引數),而前一篇介紹對位元組碼的定義已經進行了詳細介紹,執行模板定義的是每個位元組碼如何在解釋模式下執行的。initialize()函式的實現如下:

原始碼位置:/src/share/vm/interpreter/templateInterpreter.cpp

void TemplateTable::initialize() {
  if (_is_initialized) return;
  
  
  _bs = Universe::heap()->barrier_set();
  
  // For better readability
  const char _    = ' ';
  const int  ____ = 0;
  const int  ubcp = 1 << Template::uses_bcp_bit;
  const int  disp = 1 << Template::does_dispatch_bit;
  const int  clvm = 1 << Template::calls_vm_bit;
  const int  iswd = 1 << Template::wide_bit;
  //                                    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           );
  // ...
  def(Bytecodes::_tableswitch         , ubcp|disp|____|____, itos, vtos, tableswitch         ,  _           );
  def(Bytecodes::_lookupswitch        , ubcp|disp|____|____, itos, itos, lookupswitch        ,  _           );
  def(Bytecodes::_ireturn             , ____|disp|clvm|____, itos, itos, _return             , itos         );
  def(Bytecodes::_lreturn             , ____|disp|clvm|____, ltos, ltos, _return             , ltos         );
  def(Bytecodes::_freturn             , ____|disp|clvm|____, ftos, ftos, _return             , ftos         );
  def(Bytecodes::_dreturn             , ____|disp|clvm|____, dtos, dtos, _return             , dtos         );
  def(Bytecodes::_areturn             , ____|disp|clvm|____, atos, atos, _return             , atos         );
  def(Bytecodes::_return              , ____|disp|clvm|____, vtos, vtos, _return             , vtos         );
  def(Bytecodes::_getstatic           , ubcp|____|clvm|____, vtos, vtos, getstatic           , f1_byte      );
  def(Bytecodes::_putstatic           , ubcp|____|clvm|____, vtos, vtos, putstatic           , f2_byte      );
  def(Bytecodes::_getfield            , ubcp|____|clvm|____, vtos, vtos, getfield            , f1_byte      );
  def(Bytecodes::_putfield            , ubcp|____|clvm|____, vtos, vtos, putfield            , f2_byte      );
  def(Bytecodes::_invokevirtual       , ubcp|disp|clvm|____, vtos, vtos, invokevirtual       , f2_byte      );
  def(Bytecodes::_invokespecial       , ubcp|disp|clvm|____, vtos, vtos, invokespecial       , f1_byte      );
  def(Bytecodes::_invokestatic        , ubcp|disp|clvm|____, vtos, vtos, invokestatic        , f1_byte      );
  def(Bytecodes::_invokeinterface     , ubcp|disp|clvm|____, vtos, vtos, invokeinterface     , f1_byte      );
  def(Bytecodes::_invokedynamic       , ubcp|disp|clvm|____, vtos, vtos, invokedynamic       , f1_byte      );
  def(Bytecodes::_new                 , ubcp|____|clvm|____, vtos, atos, _new                ,  _           );
  def(Bytecodes::_newarray            , ubcp|____|clvm|____, itos, atos, newarray            ,  _           );
  def(Bytecodes::_anewarray           , ubcp|____|clvm|____, itos, atos, anewarray           ,  _           );
  def(Bytecodes::_arraylength         , ____|____|____|____, atos, itos, arraylength         ,  _           );
  def(Bytecodes::_athrow              , ____|disp|____|____, atos, vtos, athrow              ,  _           );
  def(Bytecodes::_checkcast           , ubcp|____|clvm|____, atos, atos, checkcast           ,  _           );
  def(Bytecodes::_instanceof          , ubcp|____|clvm|____, atos, itos, instanceof          ,  _           );
  def(Bytecodes::_monitorenter        , ____|disp|clvm|____, atos, vtos, monitorenter        ,  _           );
  def(Bytecodes::_monitorexit         , ____|____|clvm|____, atos, vtos, monitorexit         ,  _           );
  def(Bytecodes::_wide                , ubcp|disp|____|____, vtos, vtos, wide                ,  _           );
  def(Bytecodes::_multianewarray      , ubcp|____|clvm|____, vtos, atos, multianewarray      ,  _           );
  def(Bytecodes::_ifnull              , ubcp|____|clvm|____, atos, vtos, if_nullcmp          , equal        );
  def(Bytecodes::_ifnonnull           , ubcp|____|clvm|____, atos, vtos, if_nullcmp          , not_equal    );
  def(Bytecodes::_goto_w              , ubcp|____|clvm|____, vtos, vtos, goto_w              ,  _           );
  def(Bytecodes::_jsr_w               , ubcp|____|____|____, vtos, vtos, jsr_w               ,  _           );
  
  // wide Java spec bytecodes
  def(Bytecodes::_iload               , ubcp|____|____|iswd, vtos, itos, wide_iload          ,  _           );
  def(Bytecodes::_lload               , ubcp|____|____|iswd, vtos, ltos, wide_lload          ,  _           );
  // ...
  
  // JVM bytecodes
  // ...
  
  def(Bytecodes::_shouldnotreachhere   , ____|____|____|____, vtos, vtos, shouldnotreachhere ,  _           );
}

TemplateTable的初始化呼叫def()將所有位元組碼的目的碼生成函式和引數儲存在_template_table或_template_table_wide(wide指令)模板陣列中。除了虛擬機器規範本身定義的位元組碼指令外,HotSpot虛擬機器也定義了一些位元組碼指令,這些指令為了輔助虛擬機器進行更好的功能實現,例如Bytecodes::_return_register_finalizer等在之前已經介紹過,可以更好的實現finalizer型別物件的註冊功能。

我們只給出部分位元組碼指令的模板定義,呼叫def()函式對每個位元組碼指令的模板進行定義,傳遞的引數是我們關注的重點:

(1)指出為哪個位元組碼指令定義模板
(2)ubcp|disp|clvm|iswd,這是一個組合數字,具體的數字與Template中定義的列舉類緊密相關,列舉類中定義的常量如下:

enum Flags {
 uses_bcp_bit,        // set if template needs the bcp pointing to bytecode
 does_dispatch_bit,   // set if template dispatches on its own 就其本身而言; 靠自己
 calls_vm_bit,        // set if template calls the vm
 wide_bit             // set if template belongs to a wide instruction
};

下面詳細解釋這幾個引數,如下:

  • uses_bcp_bit,標誌需要使用位元組碼指標(byte code pointer,數值為位元組碼基址+位元組碼偏移量)。表示生成的模板程式碼中是否需要使用指向位元組碼指令的指標,其實也就是說是否需要讀取位元組碼指令的運算元,所以含有運算元的指令大部分都需要bcp,但是有一些是不需要的,如monitorenter與monitorexit等,這些的運算元都在表示式棧中,表示式棧頂就是其運算元,並不需要從Class檔案中讀取,所以不需要bcp;
  • does_dispatch_bit,標誌表示自己是否含有控制流轉發邏輯,如tableswitch、lookupswitch、invokevirtual、ireturn等位元組碼指令,本身就需要進行控制流轉發;
  • calls_vm_bit,標誌是否需要呼叫JVM函式,在呼叫TemplateTable::call_VM()函式時都會判斷是否有這個標誌,通常方法呼叫JVM函式時都會通過呼叫TemplateTable::call_VM()函式來間接完成呼叫。JVM函式就是用C++寫的函式。
  • wide_bit,標誌是否是wide指令(使用附加位元組擴充套件全域性變數索引)

(3)_tos_in與_tos_out:表示模板執行前與模板執行後的TosState(運算元棧棧頂元素的資料型別,TopOfStack,用來檢查模板所宣告的輸出輸入型別是否和該函式一致,以確保棧頂元素被正確使用)。

_tos_in與_tos_out的值必須是列舉類中定義的常量,如下:

enum TosState {         // describes the tos cache contents
  btos = 0,             // byte, bool tos cached
  ctos = 1,             // char tos cached
  stos = 2,             // short tos cached
  itos = 3,             // int tos cached
  ltos = 4,             // long tos cached
  ftos = 5,             // float tos cached
  dtos = 6,             // double tos cached
  atos = 7,             // object cached
  vtos = 8,             // tos not cached
  number_of_states,
  ilgl                  // illegal state: should not occur
};

如iload指令,執行之前棧頂狀態為vtos,表示並不會使用棧頂的資料,所以如果程式為了提高執行效率將上一次執行的結果快取到了暫存器中,那麼此時就應該在執行iload指令之前將這個暫存器的值壓入棧頂。iload指令執行之後的棧頂狀態為itos,因為iload是向運算元棧中壓入一個整數,所以此時的棧頂狀態為int型別,那麼這個值可以快取到暫存器中,假設下一個指令為ireturn,那麼棧頂之前與之後的狀態分別為itos和itos,那麼可直接將快取在暫存器中的int型別返回即可,不需要做任何和運算元棧相關的操作。 

(4)_gen與_arg:_gen表示模板生成器(函式指標),這個函式會為對應的位元組碼生成對應的執行邏輯;_arg表示為模板生成器傳遞的引數。呼叫函式指標會為每個位元組碼指令按其語義針對不同的平臺上生成不同的機器指令,這裡我們只討論x86架構下64位的機器指令實現,由於機器指令很難讀懂,所以我們後續只閱讀由機器指令反編譯的彙編指令。

下面看一下TemplateTable::initialize()函式中呼叫的Template::def()函式,如下:

void TemplateTable::def(
  Bytecodes::Code code,    // 位元組碼指令
  int flags,               // 標誌位
  TosState in,             // 模板執行前TosState
  TosState out,            // 模板執行後TosState
  void (*gen)(int arg),    // 模板生成器,是模板的核心元件
  int arg 
) {
  // 表示是否需要bcp指標
  const int ubcp = 1 << Template::uses_bcp_bit;     
  // 表示是否在模板範圍內進行轉發
  const int disp = 1 << Template::does_dispatch_bit; 
  // 表示是否需要呼叫JVM函式
  const int clvm = 1 << Template::calls_vm_bit;   
  // 表示是否為wide指令   
  const int iswd = 1 << Template::wide_bit;          
  
  // 如果是允許在位元組碼指令前加wide位元組碼指令的一些指令,那麼
  // 會使用_template_table_wild模板陣列進行位元組碼轉發,否則
  // 使用_template_table模板陣列進行轉發
  bool is_wide = (flags & iswd) != 0;
  Template* t = is_wide ? template_for_wide(code) : template_for(code);
  
  // 呼叫模板表t的initialize()方法初始化模板表
  t->initialize(flags, in, out, gen, arg); 
}

模板表由模板表陣列與一組生成器組成:

模板陣列有_template_table與_template_table_wild,陣列的下標為位元組碼的Opcode,值為Template。定義如下:

Template  TemplateTable::_template_table[Bytecodes::number_of_codes];
Template TemplateTable::_template_table_wide[Bytecodes::number_of_codes];

模板陣列的值為Template,這個Template類中定義了儲存標誌位flags的_flags屬性,儲存棧頂快取狀態in和out的_tos_in和_tos_out,還有儲存生成器gen及引數arg的_gen與_arg,所以呼叫t->initialize()後其實是初始化Template中的變數。initialize()函式的實現如下:

void Template::initialize(
 int flags, 
 TosState tos_in, 
 TosState tos_out, 
 generator gen, 
 int arg
) {
  _flags   = flags;
  _tos_in  = tos_in;
  _tos_out = tos_out;
  _gen     = gen;
  _arg     = arg;
}

不過這裡並不會呼叫gen函式生成對應的彙編程式碼,只是將傳遞給def()函式的各種資訊儲存到Template例項中,在TemplateTable::def()函式中,通過template_for()或template_for_wild()函式獲取到陣列中對應的Template例項後,就會呼叫Template::initialize()函式將資訊儲存到對應的Template例項中,這樣就可以根據位元組碼索引從陣列中獲取對應的Template例項,進而獲取位元組碼指令模板的相關資訊。

雖然這裡並不會呼叫gen來生成位元組碼指令對應的機器指令,但是我們可以提前看一下gen這個指標函式是怎麼為某個位元組碼指令生成對應的機器指令的。  

看一下TemplateTable::initialize()函式中對def()函式的呼叫,以_iinc(將區域性變數表中對應的slot位的值增加1)為例,呼叫如下:

def(
 Bytecodes::_iinc,     // 位元組碼指令
 ubcp|____|clvm|____,  // 標誌
 vtos,                 // 模板執行前的TosState
 vtos,                 // 模板執行後的TosState
 iinc ,                // 模板生成器,是一個iinc()函式的指標
 _                     // 不需要模板生成器引數
); 

設定標誌位uses_bcp_bit和calls_vm_bit,表示iinc指令的生成器需要使用bcp指標函式at_bcp(),且需要呼叫JVM函式,下面給出了生成器的定義:

原始碼位置:/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp

void TemplateTable::iinc() {
  transition(vtos, vtos);
  __ load_signed_byte(rdx, at_bcp(2)); // get constant
  locals_index(rbx);
  __ addl(iaddress(rbx), rdx);
}

由於iinc指令只涉及到對區域性變數表的操作,並不會影響運算元棧,也不需要使用運算元棧頂的值,所以棧頂之前與之後的狀態為vtos與vtos,呼叫transition()函式只是驗證棧頂快取的狀態是否正確。

iinc指令的位元組碼格式如下:

iinc
index // 區域性變數表索引值
const // 將區域性變數表索引值對應的slot值加const

操作碼iinc佔用一個位元組,而index與const分別佔用一個位元組。使用at_bcp()函式獲取iinc指令的運算元,2表示偏移2位元組,所以會將const取出來儲存到rdx中。呼叫locals_index()函式取出index,locals_index()就是JVM函式。最終生成的彙編如下:

// %r13儲存的是指向位元組碼的指標,偏移
// 2位元組後取出const儲存到%edx
movsbl 0x2(%r13),%edx
// 取出index儲存到%ebx
movzbl 0x1(%r13),%ebx
neg    %rbx
// %r14指向本地變數表的首地址,將%edx加到
// %r14+%rbx*8指向的記憶體所儲存的值上
// 之所以要對%rbx執行neg進行符號反轉,
// 是因為在Linux核心的作業系統上,
// 棧是向低地址方向生長的
add    %edx,(%r14,%rbx,8)

註釋解釋的已經非常清楚了,這裡不再過多介紹。  


第11篇-認識Stub與StubQueue

第10篇-初始化模板表我們介紹過TemplateInterpreter::initialize()函式,在這個函式中會呼叫TemplateTable::initialize()函式初始化模板表,隨後會使用new關鍵字初始化定義在AbstractInterpreter類中的_code靜態屬性,如下:

static StubQueue* _code;

由於TemplateInterpreter繼承自AbstractInterpreter,所以在TemplateInterpreter中初始化的_code屬性其實就是AbstractInterpreter類中定義的_code屬性。

在initialize()函式中初始化_code變數的程式碼如下:

// InterpreterCodeSize是在平臺相關
// 的templateInterpreter_x86.hpp中
// 定義的,64位下是256 * 1024
int code_size = InterpreterCodeSize;
_code = new StubQueue(
                new InterpreterCodeletInterface, 
                code_size, 
                NULL,
                "Interpreter");

StubQueue是用來儲存生成的原生程式碼的Stub佇列,佇列每一個元素對應一個InterpreterCodelet物件,InterpreterCodelet物件繼承自抽象基類Stub,包含了位元組碼對應的原生程式碼以及一些除錯和輸出資訊。下面我們介紹一下StubQueue類及相關類Stub、InterpreterCodelet類和CodeletMark類。

1、InterpreterCodelet與Stub類

Stub類的定義如下:

class Stub VALUE_OBJ_CLASS_SPEC { ... };

InterpreterCodelet類繼承自Stub類,具體的定義如下:

class InterpreterCodelet: public Stub {
 private:
  int                _size;         // the size in bytes
  const char*        _description;  // a description of the codelet, for debugging & printing
  Bytecodes::Code    _bytecode;     // associated bytecode if any
 
 public:
  // Code info
  address code_begin() const  {
     return (address)this + round_to(sizeof(InterpreterCodelet), CodeEntryAlignment);
  }
  address code_end() const {
     return (address)this + size();
  }
 
  int size() const {
     return _size;
  }
  // ...
  int code_size() const { 
     return code_end() - code_begin();  
  }
  // ...
};

InterpreterCodelet例項儲存在StubQueue中,每個InterpreterCodelet例項都代表一段機器指令(包含了位元組碼對應的機器指令片段以及一些除錯和輸出資訊),如每個位元組碼都有一個InterpreterCodelet例項,所以在解釋執行時,如果要執行某個位元組碼,則執行的就是由InterpreterCodelet例項代表的機器指令片段。

類中定義了3個屬性及一些函式,其記憶體佈局如下圖所示。

在對齊至CodeEntryAlignment後,緊接著InterpreterCodelet的就是生成的目的碼。

2、StubQueue類

StubQueue是用來儲存生成的本地機器指令片段的Stub佇列,佇列每一個元素都是一個InterpreterCodelet例項。

StubQueue類的定義如下:

class StubQueue: public CHeapObj<mtCode> {
 private:
  StubInterface* _stub_interface;     // the interface prototype
  address        _stub_buffer;        // where all stubs are stored
 
  int            _buffer_size;       // the buffer size in bytes
  int            _buffer_limit;      // the (byte) index of the actual buffer limit (_buffer_limit <= _buffer_size)
 
  int            _queue_begin;       // the (byte) index of the first queue entry (word-aligned)
  int            _queue_end;         // the (byte) index of the first entry after the queue (word-aligned)
 
  int            _number_of_stubs;   // the number of buffered stubs
 
 
  bool is_contiguous() const {
      return _queue_begin <= _queue_end;
  }
  int index_of(Stub* s) const {
      int i = (address)s - _stub_buffer;
      return i;
  }
  Stub* stub_at(int i) const {
      return (Stub*)(_stub_buffer + i);
  }
  Stub* current_stub() const {
      return stub_at(_queue_end);
  }
 
  // ...
}

這個類的建構函式如下:

StubQueue::StubQueue(
 StubInterface* stub_interface,  // InterpreterCodeletInterface物件
 int            buffer_size,     // 256*1024
 Mutex*         lock,
 const char*    name) : _mutex(lock)
{
  intptr_t     size = round_to(buffer_size, 2*BytesPerWord); // BytesPerWord的值為8
  BufferBlob*  blob = BufferBlob::create(name, size); // 在StubQueue中建立BufferBlob物件
 
  _stub_interface  = stub_interface;
 
  _buffer_size     = blob->content_size();
  _buffer_limit    = blob->content_size();
  _stub_buffer     = blob->content_begin();
 
  _queue_begin     = 0;
  _queue_end       = 0;
  _number_of_stubs = 0;
}

stub_interface用來儲存一個InterpreterCodeletInterface型別的例項,InterpreterCodeletInterface類中定義了操作Stub的函式,避免了在Stub中定義虛擬函式。每個StubQueue都有一個InterpreterCodeletInterface,可以通過這個來操作StubQueue中儲存的每個Stub例項。

呼叫BufferBlob::create()函式為StubQueue分配記憶體,這裡我們需要記住StubQueue用的記憶體是通過BufferBlob分配出來的,也就是BufferBlob其本質可能是一個StubQueue。下面就來詳細介紹下create()函式。

BufferBlob* BufferBlob::create(const char* name, int buffer_size) {
  // ...
  BufferBlob*    blob = NULL;
  unsigned int   size = sizeof(BufferBlob);
 
  // align the size to CodeEntryAlignment
  size = align_code_offset(size);
  size += round_to(buffer_size, oopSize); // oopSize是一個指標的寬度,在64位上就是8
 
  {
     MutexLockerEx mu(CodeCache_lock, Mutex::_no_safepoint_check_flag);
     blob = new (size) BufferBlob(name, size);
  }
 
  return blob;
}

通過new關鍵字為BufferBlob分配記憶體,new過載運算子如下:

void* BufferBlob::operator new(size_t s, unsigned size, bool is_critical) throw() {
  void* p = CodeCache::allocate(size, is_critical);
  return p;
}

從codeCache中分配記憶體,CodeCache使用的是本地記憶體,有自己的記憶體管理辦法,在後面將會詳細介紹。

StubQueue的佈局結構如下圖所示。

佇列中的InterpreterCodelet表示一個小例程,比如iconst_1對應的機器碼,invokedynamic對應的機器碼,異常處理對應的程式碼,方法入口點對應的程式碼,這些程式碼都是一個個InterpreterCodelet。整個直譯器都是由這些小塊程式碼例程組成的,每個小塊例程完成直譯器的部分功能,以此實現整個直譯器。


第12篇-認識CodeletMark

InterpreterCodelet依賴CodeletMark完成自動建立和初始化。CodeletMark繼承自ResourceMark,允許自動析構,執行的主要操作就是,會按照InterpreterCodelet中儲存的實際機器指令片段分配記憶體並提交。這個類的定義如下:

class CodeletMark: ResourceMark {
 private:
  InterpreterCodelet*           _clet; // InterpreterCodelet繼承自Stub
  InterpreterMacroAssembler**   _masm;
  CodeBuffer                    _cb;
  
 public:
  // 建構函式
  CodeletMark(
     InterpreterMacroAssembler*&    masm,
     const char*                    description,
     Bytecodes::Code                bytecode = Bytecodes::_illegal):
      // AbstractInterpreter::code()獲取的是StubQueue*型別的值,呼叫request()方法獲取的
      // 是Stub*型別的值,呼叫的request()方法實現在vm/code/stubs.cpp檔案中
      _clet( (InterpreterCodelet*)AbstractInterpreter::code()->request(codelet_size()) ),
      _cb(_clet->code_begin(), _clet->code_size()) 
  {
  
     // 初始化InterpreterCodelet中的_description和_bytecode屬性
     _clet->initialize(description, bytecode);
  
     // InterpreterMacroAssembler->MacroAssembler->Assembler->AbstractAssembler
     // 通過傳入的cb.insts屬性的值來初始化AbstractAssembler的_code_section與_oop_recorder屬性的值
     // create assembler for code generation
     masm  = new InterpreterMacroAssembler(&_cb); // 在建構函式中,初始化r13指向bcp、r14指向本地區域性變數表
     _masm = &masm;
  }
  
  // ... 省略解構函式
};

在建構函式中主要完成2個任務:

(1)初始化InterpreterCodelet型別的變數_clet。對InterpreterCodelet例項中的3個屬性賦值;
(2)建立一個InterpreterMacroAssembler例項並賦值給masm與_masm,此例項會通過CodeBuffer向InterpreterCodelet例項寫入機器指令。

在解構函式中,通常在程式碼塊結束時會自動呼叫解構函式,在解構函式中完成InterpreterCodelet使用的記憶體的提交併清理相關變數的值。

1、CodeletMark建構函式

在CodeletMark建構函式會從StubQueue中為InterpreterCodelet分配記憶體並初始化相關變數

在初始化_clet變數時,呼叫AbstractInterpreter::code()方法返回AbstractInterpreter類的_code屬性的值,這個值在之前TemplateInterpreter::initialize()方法中已經初始化了。繼續呼叫StubQueue類中的request()方法,傳遞的就是要求分配的用來儲存code的大小,通過呼叫codelet_size()函式來獲取,如下:

int codelet_size() {
  // Request the whole code buffer (minus a little for alignment).
  // The commit call below trims it back for each codelet.
  int codelet_size = AbstractInterpreter::code()->available_space() - 2*K;
  
  return codelet_size;
}

需要注意,在建立InterpreterCodelet時,會將StubQueue中剩下的幾乎所有可用的記憶體都分配給此次的InterpreterCodelet例項,這必然會有很大的浪費,不過我們在解構函式中會按照InterpreterCodelet例項的例項大小提交記憶體的,所以不用擔心浪費這個問題。這麼做的主要原因就是讓各個InterpreterCodelet例項在記憶體中連續存放,這樣有一個非常重要的應用,那就是隻要簡單通過pc判斷就可知道棧幀是否為解釋棧幀了,後面將會詳細介紹。

通過呼叫StubQueue::request()函式從StubQueue中分配記憶體。函式的實現如下:

Stub* StubQueue::request(int  requested_code_size) {
 
  Stub* s = current_stub();
 
  int x = stub_code_size_to_size(requested_code_size);
  int requested_size = round_to( x , CodeEntryAlignment);  // CodeEntryAlignment=32
 
  // 比較需要為新的InterpreterCodelet分配的記憶體和可用記憶體的大小情況
  if (requested_size <= available_space()) {
    if (is_contiguous()) { // 判斷_queue_begin小於等於_queue_end時,函式返回true
      // Queue: |...|XXXXXXX|.............|
      //        ^0  ^begin  ^end          ^size = limit
      assert(_buffer_limit == _buffer_size, "buffer must be fully usable");
      if (_queue_end + requested_size <= _buffer_size) {
        // code fits in(適應) at the end => nothing to do
        CodeStrings  strings;
        stub_initialize(s, requested_size, strings);
        return s; // 如果夠的話就直接返回
      } else {
        // stub doesn't fit in at the queue end
        // => reduce buffer limit & wrap around
        assert(!is_empty(), "just checkin'");
        _buffer_limit = _queue_end;
        _queue_end = 0;
      }
    }
  }
 
  // ...
 
  return NULL;
}

通過如上的函式,我們能夠清楚看到如何從StubQueue中分配InterpreterCodelet記憶體的邏輯。

首先計算此次需要從StubQueue中分配的記憶體大小,呼叫的相關函式如下:

呼叫的stub_code_size_to_size()函式的實現如下:

// StubQueue類中定義的函式
int stub_code_size_to_size(int code_size) const {  
  return _stub_interface->code_size_to_size(code_size);
}
 
// InterpreterCodeletInterface類中定義的函式
virtual int  code_size_to_size(int code_size) const { 
    return InterpreterCodelet::code_size_to_size(code_size);
}
 
// InterpreterCodelet類中定義的函式
static  int code_size_to_size(int code_size) { 
  // CodeEntryAlignment = 32
  // sizeof(InterpreterCodelet)  = 32
  return round_to(sizeof(InterpreterCodelet), CodeEntryAlignment) + code_size;
}

通過如上的分配記憶體大小的方式可知記憶體結構如下:

在StubQueue::request()函式中計算出需要從StubQueue中分配的記憶體大小後,下面進行記憶體分配。StubQueue::request()函式只給出了最一般的情況,也就是假設所有的InterpreterCodelet例項都是從StubQueue的_stub_buffer地址開始連續分配的。is_contiguous()函式用來判斷區域是否連續,實現如下:

bool is_contiguous() const {
  return _queue_begin <= _queue_end;
} 

呼叫的available_space()函式得到StubQueue可用區域的大小,實現如下:

// StubQueue類中定義的方法
int available_space() const {
 int d = _queue_begin - _queue_end - 1;
 return d < 0 ? d + _buffer_size : d;
} 

呼叫如上函式後得到的大小為下圖的黃色區域部分。

繼續看StubQueue::request()函式,當能滿足此次InterpreterCodelet例項要求的記憶體大小時,會呼叫stub_initialize()函式,此函式的實現如下:

// 下面都是通過stubInterface來操作Stub的
void  stub_initialize(Stub* s, int size,CodeStrings& strings)    {
  // 通過_stub_interface來操作Stub,會呼叫s的initialize()函式
  _stub_interface->initialize(s, size, strings);
}
 
// 定義在InterpreterCodeletInterface類中函式
virtual void  initialize(Stub* self, int size,CodeStrings& strings){
  cast(self)->initialize(size, strings);
}  
 
// 定義在InterpreterCodelet類中的函式
void initialize(int size,CodeStrings& strings) {
  _size = size;
}

我們通過StubInterface類中定義的函式來操作Stub,至於為什麼要通過StubInterface來操作Stub,就是因為Stub例項很多,所以為了避免在Stub中寫虛擬函式(C++中對含有虛擬函式的類需要分配一個指標的空間指向虛擬函式表)浪費記憶體空間而採取的辦法。

如上3個函式最終只完成了一件事兒,就是將此次分配到的記憶體大小記錄在InterpreterCodelet的_size屬性中。前面在介紹函式codelet_size()時提到過,這個值在儲存了機器指令片段後通常還會空餘很多空間,不過不要著急,下面要介紹的解構函式會根據InterpreterCodelet例項中實際生成的機器指令的大小更新這個屬性值。

2、CodeletMark解構函式

解構函式的實現如下:

// 解構函式
~CodeletMark() {
   // 對齊InterpreterCodelet
   (*_masm)->align(wordSize);
  
   // 確保生成的所有機器指令片段都儲存到了InterpreterCodelet例項中
   (*_masm)->flush();
  
   // 更新InterpreterCodelet例項的相關屬性值
   AbstractInterpreter::code()->commit((*_masm)->code()->pure_insts_size(), (*_masm)->code()->strings());
  
   // 設定_masm,這樣就無法通過這個值繼續向此InterpreterCodelet例項中生成機器指令了
   *_masm = NULL;
}

呼叫AbstractInterpreter::code()函式獲取StubQueue。呼叫(*_masm)->code()->pure_insts_size()獲取的就是InterpreterCodelet例項的機器指令片段實際需要的記憶體大小。

StubQueue::commit()函式的實現如下:  

void StubQueue::commit(int committed_code_size, CodeStrings& strings) {
  int x = stub_code_size_to_size(committed_code_size);
  int committed_size = round_to(x, CodeEntryAlignment);
 
  Stub* s = current_stub();
  assert(committed_size <= stub_size(s), "committed size must not exceed requested size");
 
  stub_initialize(s, committed_size, strings);
  _queue_end += committed_size;
  _number_of_stubs++;
}

呼叫stub_initialize()函式通過InterpreterCodelet例項的_size屬性記錄此例項中機器指令片段實際記憶體大小。同時更新StubQueue的_queue_end和_number_of_stubs屬性的值,這樣就可以為下次InterpreterCodelet例項繼續分配記憶體了。


第13篇-通過InterpreterCodelet儲存機器指令片段

在TemplateInterpreterGenerator::generate_all()函式中生成了許多位元組碼指令以及一些虛擬機器輔助執行的機器指令片段,例如生成空指標異常丟擲入口的實現如下:

{
    CodeletMark cm(_masm, "throw exception entrypoints");
    // ...
    Interpreter::_throw_NullPointerException_entry = generate_exception_handler("java/lang/NullPointerException",NULL);
    // ...
}

呼叫generate_exception_handler()函式生成丟擲空指標的程式碼片段。

address generate_exception_handler(const char* name, const char* message) {
    return generate_exception_handler_common(name, message, false);
}

呼叫的generate_exception_handler_common()函式的實現如下:

address TemplateInterpreterGenerator::generate_exception_handler_common(
const char* name, 
const char* message, 
bool pass_oop
) {
 
  assert(!pass_oop || message == NULL, "either oop or message but not both");
  address entry = __ pc();
  if (pass_oop) {
    // object is at TOS
    __ pop(c_rarg2);
  }
 
  // expression stack must be empty before entering the VM if an
  // exception happened
  __ empty_expression_stack();
 
  // setup parameters
  __ lea(c_rarg1, ExternalAddress((address)name));
 
  if (pass_oop) {
    __ call_VM(rax,
               CAST_FROM_FN_PTR(address,InterpreterRuntime::create_klass_exception),
               c_rarg1,c_rarg2);
  } else {
    // kind of lame ExternalAddress can't take NULL because
    // external_word_Relocation will assert.
    if (message != NULL) {
      __ lea(c_rarg2, ExternalAddress((address)message));
    } else {
      __ movptr(c_rarg2, NULL_WORD);
    }
    __ call_VM(rax,
               CAST_FROM_FN_PTR(address, InterpreterRuntime::create_exception),
               c_rarg1, c_rarg2);
  }
 
  // throw exception
  __ jump(ExternalAddress(Interpreter::throw_exception_entry()));
 
  return entry;
}

生成的彙編程式碼如下:

0x00007fffe10101cb: mov    -0x40(%rbp),%rsp
0x00007fffe10101cf: movq   $0x0,-0x10(%rbp)
0x00007fffe10101d7: movabs $0x7ffff6e09878,%rsi
0x00007fffe10101e1: movabs $0x0,%rdx
0x00007fffe10101eb: callq  0x00007fffe10101f5
0x00007fffe10101f0: jmpq   0x00007fffe1010288
0x00007fffe10101f5: lea    0x8(%rsp),%rax
0x00007fffe10101fa: mov    %r13,-0x38(%rbp)
0x00007fffe10101fe: mov    %r15,%rdi
0x00007fffe1010201: mov    %rbp,0x200(%r15)
0x00007fffe1010208: mov    %rax,0x1f0(%r15)
0x00007fffe101020f: test   $0xf,%esp
0x00007fffe1010215: je     0x00007fffe101022d
0x00007fffe101021b: sub    $0x8,%rsp
0x00007fffe101021f: callq  0x00007ffff66b3fbc
0x00007fffe1010224: add    $0x8,%rsp
0x00007fffe1010228: jmpq   0x00007fffe1010232
0x00007fffe101022d: callq  0x00007ffff66b3fbc
0x00007fffe1010232: movabs $0x0,%r10
0x00007fffe101023c: mov    %r10,0x1f0(%r15)
0x00007fffe1010243: movabs $0x0,%r10
0x00007fffe101024d: mov    %r10,0x200(%r15)
0x00007fffe1010254: cmpq   $0x0,0x8(%r15)
0x00007fffe101025c: je     0x00007fffe1010267
0x00007fffe1010262: jmpq   0x00007fffe1000420
0x00007fffe1010267: mov    0x250(%r15),%rax
0x00007fffe101026e: movabs $0x0,%r10
0x00007fffe1010278: mov    %r10,0x250(%r15)
0x00007fffe101027f: mov    -0x38(%rbp),%r13
0x00007fffe1010283: mov    -0x30(%rbp),%r14
0x00007fffe1010287: retq   
0x00007fffe1010288: jmpq   0x00007fffe100f3d3

在這裡的重點不是讀懂TemplateInterpreterGenerator::generate_exception_handler_common()函式的邏輯及生成的彙編程式碼,而是要清楚知道CodeletMark的應用,以及generate_exception_handler_common()函式生成的機器指令是如何寫入InterpreterCodelet例項中的。之前介紹過InterpreterCodelet與CodeBuffer類,如下:

通過CodeBuffer操作InterpreterCodelet例項的儲存機器指令片段的記憶體區域,而CodeBuffer中的程式碼部分(CodeSection)被賦值給AbstractAssembler::_code_section。這樣我們就可以通過_code_section屬性向InterpreterCodelet例項中寫入機器指令了。

向CodeletMark中傳入的_masm引數定義在AbstractInterpreterGenerator類中,如下:

class AbstractInterpreterGenerator: public StackObj {
   protected:
      InterpreterMacroAssembler* _masm;
      // ...
}

generate_exception_handler_common()函式中的__是一個巨集,定義如下:

#define __ _masm->

這樣其實就是呼叫InterpreterMacroAssembler類中的相關函式寫機器指令,例如

__ pop(c_rarg2);

呼叫的pop()函式如下:

// 定義在InterpreterMacroAssembler中
void pop(Register r ) {
  ((MacroAssembler*)this)->pop(r);
}
 
// 定義在Assembler類中
void Assembler::pop(Register dst) {
  int encode = prefix_and_encode(dst->encoding());
  emit_int8(0x58 | encode);
}
 
// 定義在AbstractAssembler類中
void emit_int8(   int8_t  x) { 
   code_section()->emit_int8(   x); 
}

code_section()函式獲取的就是AbstractAssembler的_code_section屬性的值。  


第14篇-生成重要的例程

之前介紹過TemplateInterpreter::initialize()函式,在這個函式中初始化了模板表和StubQueue例項,通過如下方式建立InterpreterGenerator例項:

InterpreterGenerator g(_code);

在建立InterpreterGenerator例項時會呼叫generate_all()函式,如下:

InterpreterGenerator::InterpreterGenerator(StubQueue* code)
  : TemplateInterpreterGenerator(code) {
   generate_all(); 
}

在generate_all()函式中生成各種例程(機器指令片段)並儲存到Interpretercodelet例項中。在HotSpot VM中,不僅有位元組碼對應的例程,還有許多輔助虛擬機器執行時的例程,如之前介紹的普通方法入口entry_point例程,處理異常的例程等等。這些例程都會儲存到StubQueue中,如下圖所示。

生成的一些重要例程如下表所示。

其中非本地方法的入口、本地方法的入口和位元組碼的入口比較重要,也是我們後面介紹的重點內容。 這一篇介紹非本地方法的入口和位元組碼的入口,對於本地方法的入口將在介紹本地方法時詳細介紹,這裡不過多介紹。

1、非本地方法入口

我們在之前介紹為非本地的普通方法建立Java棧幀的時候提到過,主要的非本地方法入口有如下幾類:

enum MethodKind {
    zerolocals,  // 普通的方法             
    zerolocals_synchronized,  // 普通的同步方法         
    ...
}

在generate_all()函式中生成普通方法和普通的同步方法的入口邏輯如下:

{
 CodeletMark cm(_masm, "method entry point (kind = " "zerolocals" ")");
 Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);
}
{
 CodeletMark cm(_masm, "method entry point (kind = " "zerolocals_synchronized" ")");
 Interpreter::_entry_table[Interpreter::zerolocals_synchronized] = generate_method_entry(Interpreter::zerolocals_synchronized);
}

呼叫的generate_method_entry()函式在第6篇已經詳細介紹過,最終會生成建立Java棧幀的例程,將例程的首地址儲存到Interpreter::_entry_table陣列中。

關於同步方法的棧幀建立及特殊邏輯的處理將在介紹鎖相關知識時詳細介紹,這裡不在過多介紹。

除了普通的方法外,還為一些方法生成了一些特殊的入口地址,如為java.lang.Math.sin()、java.lang.Math.cos()等方法生成的例程。如果大家有興趣可以自己研究一下,這裡不在詳細介紹。

2、位元組碼入口

在generate_all()函式中會呼叫set_entry_points_for_all_bytes()函式,此函式對所有被定義的位元組碼生成例程並通過對應的屬性儲存入口,這些入口指向了例程的首地址。set_entry_points_for_all_bytes()函式的實現如下:

void TemplateInterpreterGenerator::set_entry_points_for_all_bytes() {
  for (int i = 0; i < DispatchTable::length; i++) {
     Bytecodes::Code code = (Bytecodes::Code)i;
     if (Bytecodes::is_defined(code)) {
         set_entry_points(code);
     } else {
         set_unimplemented(i);
     }
  }
}

當code是Java虛擬機器規範中定義的位元組碼指令時,呼叫set_entry_points()函式,此函式取出該位元組碼指令對應的Template模板並呼叫set_short_enrty_points()函式進行處理,將入口地址儲存在轉發表(DispatchTable)_normal_table或_wentry_table(使用wide指令)中。Template模板在之前已經介紹過,位元組碼指令都會對應一個Template模板,而模板中儲存著位元組碼指令生成對應程式碼例程中需要的資訊。

set_entry_points()函式的實現如下:

void TemplateInterpreterGenerator::set_entry_points(Bytecodes::Code code) {
  CodeletMark cm(_masm, Bytecodes::name(code), code);
 
  address bep = _illegal_bytecode_sequence;
  address cep = _illegal_bytecode_sequence;
  address sep = _illegal_bytecode_sequence;
  address aep = _illegal_bytecode_sequence;
  address iep = _illegal_bytecode_sequence;
  address lep = _illegal_bytecode_sequence;
  address fep = _illegal_bytecode_sequence;
  address dep = _illegal_bytecode_sequence;
  address vep = _unimplemented_bytecode;
  address wep = _unimplemented_bytecode;
 
  // 處理非wide指令,注意指的是那些不能在前面加wide指令的位元組碼指令
  if (Bytecodes::is_defined(code)) {
     Template* t = TemplateTable::template_for(code);
     set_short_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);
  }
 
  // 處理wide指令,注意指的是那些能在前面加wide指令的位元組碼指令
  if (Bytecodes::wide_is_defined(code)) {
     Template* t = TemplateTable::template_for_wide(code);
     set_wide_entry_point(t, wep);
  }
 
  // 當為非wide指令時,共有9個入口,當為wide指令時,只有一個入口
  EntryPoint  entry(bep, cep, sep, aep, iep, lep, fep, dep, vep);
  Interpreter::_normal_table.set_entry(code, entry);
  Interpreter::_wentry_point[code] = wep;
}

注意函式開始時宣告時建立了一個變數cm,此時會呼叫CodeletMark建構函式在StubQueue中建立出儲存機器片段的InterpreterCodelet例項,所以呼叫TemplateInterpreterGenerator::set_short_entry_points()等函式生成的機器指令都會寫入到這個例項中。當函式執行完成後,CodeletMark解構函式會提交使用的記憶體並重置相關屬性值。

接下來就是為表示棧頂快取(Top-of-Stack Caching,縮寫為TOSCA,簡稱Tos)狀態的變數賦初始值,其中的_illegal_bytecode_sequence與_unimplemented_bytecode變數指向的也是特定例程的入口地址,這些例程就是在generate_all()函式中生成的,如果大家有興趣,可以研究一下這些例程是怎麼處理非法位元組碼等情況的。

呼叫set_short_entry_points()函式時,需要傳入棧頂快取狀態,也就是上一個位元組碼執行時可能會將產生的結果儲存到暫存器中。使用棧頂快取主要還是為了提高解釋執行的效率。HotSpot VM共定義了9種TosState,通過列舉常量來表示,如下:

enum TosState {      // describes the tos cache contents
  btos = 0,          // byte, bool tos cached
  ctos = 1,          // char tos cached
  stos = 2,          // short tos cached
  itos = 3,          // int tos cached
  ltos = 4,          // long tos cached
  ftos = 5,          // float tos cached
  dtos = 6,          // double tos cached
  atos = 7,          // object cached
  vtos = 8,          // tos not cached
  number_of_states,
  ilgl               // illegal state: should not occur
};

以非wide指令為例進行說明,bep(byte entry point)、cep、 sep、aep、iep、lep、fep、dep、vep分別表示指令執行前棧頂元素狀態為byte/boolean、char、short、array/reference(物件引用)、int、long、float、double、void型別時的入口地址。舉個例子,如iconst_0表示向棧中壓入常量0,那麼位元組碼指令模板中有如下定義:

def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst,0);

第3個引數指明瞭tos_in,第4個引數為tos_out,tos_in與tos_out是指令執行前後的TosState。也就是說,執行此位元組碼指令之前不需要獲取棧頂快取的值,所以為void;執行完成後棧頂會快取一個int型別的整數,也就是0。快取通常會快取到暫存器中,所以比起壓入棧中,獲取的效率要更高一些,如果下一個執行的位元組碼指令不需要,那麼還需要將快取的0值壓入棧內。假設下一個執行的位元組碼也為iconst,那麼要從iconst指令的iep(上一個快取了int型別整數0)入口來執行,由於iconst的入口要求為vtos,所以需要將暫存器中的int型別數值0入棧。所以每個位元組碼指令都會有多個入口,這樣任何一個位元組碼指令在執行完成後,都可以根據當前執行後的棧頂快取狀態找到下一個需要執行位元組碼的對應入口。

再回頭看一下我們第8篇介紹的分發位元組碼相關內容,為各個位元組碼設定入口的函式DispatchTable::set_entry(),其中的_table的一維為棧頂快取狀態,二維為Opcode,通過這2個維度能夠找到一段機器指令,這就是根據當前的棧頂快取狀態定位到的位元組碼需要執行的例程。我們看一下TemplateInterpreterGenerator::set_entry_points()函式,最後會呼叫DispatchTable::set_entry()函式為_table屬性賦值。這樣型別為DispatchTable的TemplateInterpreter::_normal_table與TemplateInterpreter::_wentry_point變數就可以完成位元組碼分發了。

呼叫TemplateTable::template_for()函式可以從TemplateTable::_template_table陣列中獲取對應的Template例項,然後呼叫set_short_entry_points()函式生成例程。非wild指令呼叫set_short_entry_points()函式,set_short_entry_points()函式的實現如下:

void TemplateInterpreterGenerator::set_short_entry_points(
Template* t,
address& bep, address& cep, address& sep, address& aep, address& iep,
address& lep, address& fep, address& dep, address& vep
) {
  switch (t->tos_in()) {
    case btos:
    case ctos:
    case stos:
      ShouldNotReachHere();  
      break;
    case atos: vep = __ pc(); __ pop(atos); aep = __ pc(); generate_and_dispatch(t);   break;
    case itos: vep = __ pc(); __ pop(itos); iep = __ pc(); generate_and_dispatch(t);   break;
    case ltos: vep = __ pc(); __ pop(ltos); lep = __ pc(); generate_and_dispatch(t);   break;
    case ftos: vep = __ pc(); __ pop(ftos); fep = __ pc(); generate_and_dispatch(t);   break;
    case dtos: vep = __ pc(); __ pop(dtos); dep = __ pc(); generate_and_dispatch(t);   break;
    case vtos: set_vtos_entry_points(t, bep, cep, sep, aep, iep, lep, fep, dep, vep);  break;
    default  : ShouldNotReachHere();                                                   break;
  }
}

set_short_entry_points()函式會根據Template例項中儲存的位元組碼模板資訊生成最多9個棧頂入口並賦值給傳入引數bep、cep等,也就是給Template代表的特定位元組碼指令生成相應的入口地址。

set_short_entry_points()函式根據運算元棧棧頂元素型別進行判斷,首先byte、char和short型別都應被當做int型別進行處理,所以不會為位元組碼指令生成這幾個型別的入口地址;如果當前位元組碼執行之前要求有棧頂元素並且型別是atos物件型別,那麼當沒有棧頂快取時,從vep入口進入,然後彈出表示式棧中的物件到棧頂快取暫存器後,就可以直接從aep進入,itos、ltos、ftos和dtos也都類似,會分別成為2個入口地址;如果不要求有棧頂元素,那麼就是vtos,非void型別將呼叫generate_and_dispatch()函式生成各種入口。

set_vtos_entry_points()函式的實現如下:

void TemplateInterpreterGenerator::set_vtos_entry_points(
 Template* t,
 address& bep,
 address& cep,
 address& sep,
 address& aep,
 address& iep,
 address& lep,
 address& fep,
 address& dep,
 address& vep) {
  Label L;
  aep = __ pc();  __ push_ptr();  __ jmp(L);
  fep = __ pc();  __ push_f();    __ jmp(L);
  dep = __ pc();  __ push_d();    __ jmp(L);
  lep = __ pc();  __ push_l();    __ jmp(L);
  bep = cep = sep =
  iep = __ pc();  __ push_i();
  vep = __ pc();
  __ bind(L);
  generate_and_dispatch(t);
}

如果位元組碼不要求有棧頂快取時(即vtos狀態),會為當前位元組碼生成9個入口地址,由bep、cep等儲存下來。如生成aep入口時,因為當前執行的位元組碼棧不需要頂快取狀態,所以要把值壓入表示式棧中,然後跳轉到L處執行,也就是相當於從vep入口進入執行了。

現在簡單梳理一下,上一個位元組碼指令到底從哪個入口進入到下一個位元組碼指令要通過上一個位元組碼指令的執行結果而定。如果上一個位元組碼指令執行的結果為fep,而當前位元組碼指令執行之前的棧頂快取狀態要求是vtos,則從TemplateInterpreterGenerator::set_vtos_entry_points()函式中給fep賦值的地方開始執行。所以說,上一個位元組碼指令的執行結果和下一個將要執行的位元組碼指令執行之前要求的棧頂快取狀態共同決定了從哪個入口進入。

push_f()函式的實現如下:

原始碼位置:/hotspot/src/cpu/x86/vm/interp_masm_x86_64.cpp

void InterpreterMacroAssembler::push_f(XMMRegister r) { // r的預設值為xmm0
    subptr(rsp, wordSize);       // wordSize為機器字長,64位下為8位元組,所以值為8
    movflt(Address(rsp, 0), r);
}
 
void MacroAssembler::subptr(Register dst, int32_t imm32) {
  LP64_ONLY(subq(dst, imm32)) NOT_LP64(subl(dst, imm32));
}
 
void Assembler::subq(Register dst, int32_t imm32) {
   (void) prefixq_and_encode(dst->encoding());
   emit_arith(0x81, 0xE8, dst, imm32);
}
 
void Assembler::emit_arith(int op1, int op2, Register dst, int32_t imm32) {
  assert(isByte(op1) && isByte(op2), "wrong opcode");
  assert((op1 & 0x01) == 1, "should be 32bit operation");
  assert((op1 & 0x02) == 0, "sign-extension bit should not be set");
  if (is8bit(imm32)) {
    emit_int8(op1 | 0x02); // set sign bit
    emit_int8(op2 | encode(dst));
    emit_int8(imm32 & 0xFF);
  } else {
    emit_int8(op1);
    emit_int8(op2 | encode(dst));
    emit_int32(imm32);
  }
}

呼叫emit_arith()、emit_int8()等函式生成機器指令片段,生成的內容最後會儲存到StubQueue的InterpreterCodelet例項中,關於機器指令和生成儲存過程在之前已經介紹過,這裡不做過多介紹。

set_vtos_entry_points()函式生成的機器指令片段經過反編譯後,對應的彙編程式碼後如下:

// aep的入口
push   %rax           
jmpq   L 
 
// fep入口
sub    $0x8,%rsp      
movss  %xmm0,(%rsp)
jmpq   L
        
// dep入口  
sub    $0x10,%rsp     
movsd  %xmm0,(%rsp)
jmpq   L
 
// lep入口
sub    $0x10,%rsp    
mov    %rax,(%rsp)
jmpq   L
 
// iep入口
push   %rax   
 
// ---- L ----     

set_vtos_entry_points()函式最後呼叫generate_and_dispatch()函式寫入當前位元組碼指令對應的機器指令片段和跳轉到下一個位元組碼指令繼續執行的邏輯處理部分。

generate_and_dispatch()函式的主要實現如下:

void TemplateInterpreterGenerator::generate_and_dispatch(Template* t, TosState tos_out) {
  // 生成當前位元組碼指令對應的機器指令片段
  t->generate(_masm);
 
  if (t->does_dispatch()) {
     // asserts
  } else {
     // 生成分發到下一個位元組碼指令的邏輯
     __ dispatch_epilog(tos_out, step);
  }
}

這裡以iconst位元組碼為例分析generate()函式的實現:

void Template::generate(InterpreterMacroAssembler* masm) {
  // parameter passing
  TemplateTable::_desc = this;
  TemplateTable::_masm = masm;
  // code generation
  _gen(_arg);
  masm->flush();
}

generate()函式會呼叫生成器函式_gen(_arg),對於iconst指令來說,生成器函式為iconst()。generate()函式根據平臺而不同,如x86_64平臺下,定義如下:

原始碼位置:/hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp

void TemplateTable::iconst(int value) {
  if (value == 0) {
    __ xorl(rax, rax);
  } else {
    __ movl(rax, value);
  }
}

我們知道,iconst_i指令是將i壓入棧,這裡生成器函式iconst()在i為0時,沒有直接將0寫入rax,而是使用異或運算清零,即向程式碼緩衝區寫入指令”xor %rax, %rax”;當i不為0時,寫入指令”mov $0xi, %rax”

當不需要轉發時,會在TemplateInterpreterGenerator::generate_and_dispatch()函式中呼叫dispatch_epilog()函式生成取下一條指令和分派的目的碼:

void InterpreterMacroAssembler::dispatch_epilog(TosState state, int step) {
   dispatch_next(state, step);
}

dispatch_next()函式的實現如下:

void InterpreterMacroAssembler::dispatch_next(TosState state, int step) {
  // load next bytecode (load before advancing r13 to prevent AGI)
  load_unsigned_byte(rbx, Address(r13, step));
  // advance r13
  increment(r13, step);
  dispatch_base(state, Interpreter::dispatch_table(state));
}

這個函式在之前已經介紹過,這裡不再介紹。


第15章-直譯器及直譯器生成器

方法解釋執行時需要直譯器與直譯器生成器的支援。直譯器與直譯器生成器的繼承體系如下:

下面詳細介紹直譯器與直譯器生成器。

1、直譯器

直譯器是一堆原生程式碼例程構造的,這些例程會在虛擬機器啟動的時候寫入到StubQueue中,以後解釋執行時就只需要進入指定例程即可。

直譯器的繼承體系如下:

AbstractInterpreter        /interpreter/abstractInterpreter.hpp
     CppInterpreter
     TemplateInterpreter   /interpreter/templateInterpreter.hpp
            Interpreter    /interpreter/templateInterpreter.hpp

Interpreter通過巨集可以繼承自CppInterpreter或者TemplateInterpreter,前者稱為C++直譯器,每個位元組碼指令都對應一段C++程式碼,通過switch的方式處理位元組碼,後者稱為模板直譯器,每個指令對應一段機器指令片段,通過指令模板的方式處理位元組碼,HotSpot VM預設使用模板直譯器。

(1)抽象直譯器AbstractInterpreter

所有的直譯器都繼承自抽象直譯器,類及重要屬性的定義如下:

class AbstractInterpreter{
   StubQueue* _code                      
   address    _entry_table[n];         
   // ...
}; 

_code屬性在之前已經介紹過,這是一個佇列,佇列中的InterpreterCodelet表示一個例程,比如iconst_1對應的程式碼,invokedynamic對應的程式碼,異常處理對應的程式碼,方法入口點對應的程式碼,這些程式碼都是一個個InterpreterCodelet。整個直譯器都是由這些例程組成的,每個例程完成直譯器的部分功能,以此實現整個直譯器。

_entry_table陣列會會儲存方法入口點,例如普通方法的入口點為_entry_table[0]、同步的普通方法的入口點為_entry_table[1],這些_entry_table[0],_entry_table[1]指向的就是之前_code佇列裡面的例程。這些邏輯都在是generate_all()函式中完成的,如下:  

void TemplateInterpreterGenerator::generate_all() {
  // ...
 
  method_entry(zerolocals)
  method_entry(zerolocals_synchronized)
  method_entry(empty)
  method_entry(accessor)
  method_entry(abstract)
  method_entry(java_lang_math_sin  )
  method_entry(java_lang_math_cos  )
  method_entry(java_lang_math_tan  )
  method_entry(java_lang_math_abs  )
  method_entry(java_lang_math_sqrt )
  method_entry(java_lang_math_log  )
  method_entry(java_lang_math_log10)
  method_entry(java_lang_math_exp  )
  method_entry(java_lang_math_pow  )
  method_entry(java_lang_ref_reference_get)
   
  // ...
}

method_entry巨集的定義如下:

#define method_entry(kind)                                                                    \
{                                                                                             \
    CodeletMark cm(_masm, "method entry point (kind = " #kind ")");                           \
    Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind);  \
}

可以看到,呼叫generate_method_entry()函式會返回例程對應的入口地址,然後儲存到AbstractInterpreter類中定義的_entry_table陣列中。呼叫generate_method_entry()函式傳入的引數是列舉常量,表示一些特殊的方法和一些常見的方法型別。

(2)模板直譯器TemplateInterpreter

模板直譯器類的定義如下:

class TemplateInterpreter: public AbstractInterpreter {
  protected:
    // 陣列越界異常例程
    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)直譯器Interpreter

類的定義如下:

class Interpreter: public CC_INTERP_ONLY(CppInterpreter) NOT_CC_INTERP(TemplateInterpreter) {
  // ...
}

沒有定義新的屬性,只有幾個函式。Interpreter預設通過巨集擴充套件的方式繼承TemplateInterpreter。

2、直譯器生成器

要想得到可執行的直譯器還需要直譯器生成器。直譯器生成器本來可以獨自完成填充工作,可能為了解耦,也可能是為了結構清晰,HotSpot VM將位元組碼的例程抽了出來放到了TemplateTable模板表中,它輔助模板直譯器生成器templateInterpreterGenerator生成各種例程。

直譯器生成器的繼承體系如下:

AbstractInterpreterGenerator        /interpreter/abstractInterpreter.hpp
     TemplateInterpreterGenerator   /interpreter/templateInterpreter.hpp
              InterpreterGenerator  /interpreter/interpreter.hpp

模板直譯器生成器擴充套件了抽象直譯器生成器。直譯器生成器與直譯器其實有某種意義上的對應關係,如抽象直譯器生成器中定義了一些函式,呼叫這些函式會初始化抽象直譯器中的屬性,如儲存例程的_entry_table陣列等,在模板直譯器生成器中定義的函式會初始化模板直譯器中定義的一些屬性,如_throw_ArrayIndexOutOfBoundsException_entry等。之前介紹過空指標的例程就是在這個TemplateInterpreterGenerator類的generate_all()函式中生成的。如下:

{
    CodeletMark cm(_masm, "throw exception entrypoints");
    // ...
    Interpreter::_throw_NullPointerException_entry = generate_exception_handler("java/lang/NullPointerException",NULL);
    // ...
}

關於直譯器生成器不再過多介紹。

這裡我們需要提醒的是,直譯器和直譯器生成器中定義的函式在實現過程中,有些和平臺是無關的,所以會在/interpreter資料夾下的檔案中實現。例如Interpreter和InterpreterGenerator類定義在/interpreter資料夾下,其中定義的函式會在/interpreter資料夾下的interpreter.cpp檔案中實現,但是有些函式是針對特定平臺,我們只討論linux在x86架構下的64位實現,所以cpu/x86/vm資料夾下也有interpreter_x86.hpp和interpreter_x86_64.cpp等檔案,只需要在定義Interpreter類時包含interpreter_x86.hpp檔案即可。


第16章-虛擬機器中的彙編器

彙編器的繼承體系如下:

為解析器提供的相關彙編介面,所以每個位元組碼指令都會關聯一個生成器函式,而生成器函式會呼叫匯編器生成機器指令片段,例如為iload位元組碼指令生成例程時,呼叫的生成函式為TemplateTable::iload(int n),此函式的實現如下:

原始碼位置:hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp

void TemplateTable::iload() {
  transition(vtos, itos);
  ...
  // Get the local value into tos
  locals_index(rbx);
  __ movl(rax, iaddress(rbx)); // iaddress(rb)為源運算元,rax為目地運算元
}

函式呼叫了__ movl(rax,iaddress(rbx))函式生成對應的機器指令,所生成的機器指令為mov reg,operand,其中__為巨集,定義如下:

原始碼位置:hotspot/src/cpu/x86/vm/templateTable_x86_64.cpp

#define __ _masm->

_masm變數的定義如下:

原始碼位置:hotspot/src/share/vm/interpreter/abstractInterpreter.hpp

class AbstractInterpreterGenerator: public StackObj {
 protected:
  InterpreterMacroAssembler* _masm;
  ...
}

最終_masm會被例項化為InterprterMacroAssembler型別。

在x86的64位平臺上呼叫movl()函式的實現如下:

原始碼位置:hotspot/src/cpu/x86/vm/assembler_86.cpp

void Assembler::movl(Register dst, Address src) {
  InstructionMark im(this);
  prefix(src, dst);
  emit_int8((unsigned char)0x8B);
  emit_operand(dst, src);
}  

呼叫prefix()、emit_int8()等定義在彙編器中的函式時,這些函式會通過AbstractAssembler::_code_section屬性向InterpreterCodelet例項中寫入機器指令,這個內容在之前已經介紹過,這裡不再介紹。

1、AbstractAssembler類

AbstractAssembler類中定義了生成彙編程式碼的抽象公共基礎函式,如獲取關聯CodeBuffer的當前記憶體位置的pc()函式,將機器指令全部重新整理到InterpreterCodelet例項中的flush()函式,繫結跳轉標籤的bind()函式等。AbstractAssembler類的定義如下:

原始碼位置:hotspot/src/share/vm/asm/assembler.hpp

// The Abstract Assembler: Pure assembler doing NO optimizations on the
// instruction level; i.e., what you write is what you get.
// The Assembler is generating code into a CodeBuffer.
class AbstractAssembler : public ResourceObj  {
  friend class Label;
 
 protected:
  CodeSection* _code_section;      // section within the code buffer
  OopRecorder* _oop_recorder;      // support for relocInfo::oop_type
 
  ...
 
  void emit_int8(int8_t  x) {
    code_section()->emit_int8(x);
  }
  void emit_int16(int16_t x) {
    code_section()->emit_int16(x);
  }
  void emit_int32(int32_t x) {
    code_section()->emit_int32(x);
  }
  void emit_int64(int64_t x) {
    code_section()->emit_int64(x);
  }
 
  void emit_float(  jfloat  x) {
    code_section()->emit_float(x);
  }
  void emit_double( jdouble x) {
    code_section()->emit_double(x);
  }
  void emit_address(address x) {
    code_section()->emit_address(x);
  }
  ...
}

彙編器會生成機器指令序列,並且將生成的指令序列儲存到快取中,而_code_begin指向快取區首地址,_code_pos指向快取區的當前可寫入的位置。

這個彙編器提供了寫機器指令的基礎函式,通過這些函式可方便地寫入8位、16位、32位和64位等的資料或指令。這個彙編器中處理的業務不會依賴於特定平臺。

2、Assembler類

assembler.hpp檔案中除定義AbstractAssembler類外,還定義了jmp跳轉指令用到的標籤Lable類,呼叫bind()函式後就會將當前Lable例項繫結到指令流中一個特定的位置,比如jmp指令接收Lable引數,就會跳轉到對應的位置處開始執行,可用於實現迴圈或者條件判斷等控制流操作。

Assembler的定義跟CPU架構有關,通過assembler.hpp檔案中的巨集包含特定CPU下的Assembler實現,如下:

原始碼位置:hotspot/src/share/vm/asm/assembler.hpp

#ifdef TARGET_ARCH_x86
# include "assembler_x86.hpp"
#endif

Assembler類新增了特定於CPU架構的指令實現和指令操作相關的列舉。 定義如下:

原始碼位置:hotspot/src/cpu/x86/vm/assembler_x86.hpp

// The Intel x86/Amd64 Assembler: Pure assembler doing NO optimizations on the instruction
// level (e.g. mov rax, 0 is not translated into xor rax, rax!); i.e., what you write
// is what you get. The Assembler is generating code into a CodeBuffer.
class Assembler : public AbstractAssembler  {
   ...
} 

提供的許多函式基本是對單個機器指令的實現,例如某個movl()函式的實現如下:

void Assembler::movl(Register dst, Address src) {
  InstructionMark im(this);
  prefix(src, dst);
  emit_int8((unsigned char)0x8B);
  emit_operand(dst, src);
}

subq()函式的實現如下:

void Assembler::subq(Register dst, int32_t imm32) {
  (void) prefixq_and_encode(dst->encoding());
  emit_arith(0x81, 0xE8, dst, imm32);
}

如上函式將會呼叫emit_arith()函式,如下:

void Assembler::emit_arith(int op1, int op2, Register dst, int32_t imm32) {
  assert(isByte(op1) && isByte(op2), "wrong opcode");
  assert((op1 & 0x01) == 1, "should be 32bit operation");
  assert((op1 & 0x02) == 0, "sign-extension bit should not be set");
 
  if (is8bit(imm32)) {
    emit_int8(op1 | 0x02); // set sign bit
    emit_int8(op2 | encode(dst));
    emit_int8(imm32 & 0xFF);
  } else {
    emit_int8(op1);
    emit_int8(op2 | encode(dst));
    emit_int32(imm32);
  }
}

呼叫emit_int8()或emit_int32()等函式寫入機器指令。最後寫入的指令如下:

83 EC 08

由於8可由8位有符號數表示,第一個位元組為0x81 | 0x02,即0x83,rsp的暫存器號為4,第二個位元組為0xE8 | 0x04,即0xEC,第三個位元組為0x08 & 0xFF,即0x08,該指令即AT&T風格的sub $0x8,%rsp。

我在這裡並不會詳細解讀彙編器中emit_arith()等函式的實現邏輯,這些函式如果在不理解機器指令編碼的情況下很難理解其實現過程。後面我們根據Intel手冊介紹了機器指令編碼格式後會選幾個典型的實現進行解讀。

3、MacroAssembler類

MacroAssembler類繼承自Assembler類,主要是新增了一些常用的彙編指令支援。類的定義如下:

原始碼位置:hotspot/src/cpu/x86/vm/macroAssembler_x86.hpp

// MacroAssembler extends Assembler by frequently used macros.
//
// Instructions for which a 'better' code sequence exists depending
// on arguments should also go in here.
 
class MacroAssembler: public Assembler {
   ...
}

這個類中的函式通過呼叫MacroAssembler類或Assembler類中定義的一些函式來完成,可以看作是通過對機器指令的組合來完成一些便於業務程式碼操作的函式。

根據一些方法傳遞的引數可知,能夠支援JVM內部資料型別級別的操作。

例如機器指令在做加法操作時,不允許兩個運算元同時都是儲存器運算元,或者一個來自記憶體,另外一個來自立即數,但是MacroAssembler彙編器中卻提供了這樣的函式。

4、InterpreterMacroAssembler類

在templateTable.hpp檔案中已經根據平臺判斷要引入的檔案了,如下:

#ifdef TARGET_ARCH_x86
# include "interp_masm_x86.hpp"
#endif

在interp_masm_x86.hpp檔案中定義了InterpreterMacroAssembler類,如下:

原始碼位置:hotspot/src/cpu/x86/vm/interp_masm_x86.hpp

// This file specializes the assember with interpreter-specific macros
 
class InterpreterMacroAssembler: public MacroAssembler {
...
#ifdef TARGET_ARCH_MODEL_x86_64
# include "interp_masm_x86_64.hpp"
#endif
...
}

對於64位平臺來說,引入了interp_masm_x86_64.hpp檔案。 

在interp_masm_x86_64.cpp檔案中定義瞭如下幾個函式:

(1)InterpreterMacroAssembler::lock_object()
(2)InterpreterMacroAssembler::unlock_object()
(3)InterpreterMacroAssembler::remove_activation()
(4)InterpreterMacroAssembler::dispatch_next()

其中的dispatch_next()函式大家應該不陌生,這個函式在之前介紹過,是為分發位元組碼指令生成例程;lock_object()和unlock_object()是在解釋執行的情況下,為載入和釋放鎖操作生成對應的例程,在後面介紹鎖相關的知識時會詳細介紹;remove_activation()函式表示移除對應的棧幀,例如在遇到異常時,如果當前的方法不能處理此異常,那就需要對棧進行破壞性展開,在展開過程中需要移除對應的棧幀。

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

相關文章