第8篇-dispatch_next()函數分派位元組碼

在generate_normal_entry()函數中會調用generate_fixed_frame()函數為Java方法的執行生成對應的棧幀,接下來還會調用dispatch_next()函數執行Java方法的位元組碼。generate_normal_entry()函數調用的dispatch_next()函數之前一些暫存器中保存的值如下:

rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地變數表第1個參數的地址

dispatch_next()函數的實現如下:

// 從generate_fixed_frame()函數生成Java方法調用棧幀的時候,
// 如果當前是第一次調用,那麼r13指向的是位元組碼的首地址,
// 即第一個位元組碼,此時的step參數為0
void InterpreterMacroAssembler::dispatch_next(TosState state, int step) {
 
  load_unsigned_byte(rbx, Address(r13, step)); 
 
  // 在當前位元組碼的位置,指針向前移動step寬度,
  // 獲取地址上的值,這個值是Opcode(範圍1~202),存儲到rbx
  // step的值由位元組碼指令和它的操作數共同決定
  // 自增r13供下一次位元組碼分派使用
  increment(r13, step);
 
  // 返回當前棧頂狀態的所有位元組碼入口點
  dispatch_base(state, Interpreter::dispatch_table(state)); 
}

r13指向位元組碼的首地址,當第1次調用時,參數step的值為0,那麼load_unsigned_byte()函數從r13指向的記憶體中取一個位元組的值,取出來的是位元組碼指令的操作碼。增加r13的步長,這樣下次執行時就會取出來下一個位元組碼指令的操作碼。

調用的dispatch_table()函數的實現如下:

static address*   dispatch_table(TosState state)  {
   return _active_table.table_for(state); 
}

在_active_table中獲取對應棧頂快取狀態的入口地址,_active_table變數定義在TemplateInterpreter類中,如下:

static DispatchTable  _active_table;  

DispatchTable類及table_for()等函數的定義如下:

DispatchTable  TemplateInterpreter::_active_table;

class DispatchTable VALUE_OBJ_CLASS_SPEC {
 public:
  enum { 
    length = 1 << BitsPerByte 
  }; // BitsPerByte的值為8
 
 private:
  // number_of_states=9,length=256
  // _table是位元組碼分發表 
  address  _table[number_of_states][length];   
 
 public:
  // ...
  address*   table_for(TosState state){ 
    return _table[state]; 
  }

  address*   table_for(){ 
    return table_for((TosState)0); 
  }
  // ...
}; 

address為u_char*類型的別名。_table是一個二維數組的表,維度為棧頂狀態(共有9種)和位元組碼(最多有256個),存儲的是每個棧頂狀態對應的位元組碼的入口點。這裡由於還沒有介紹棧頂快取,所以理解起來並不容易,不過後面會詳細介紹棧頂快取和位元組碼分發表的相關內容,等介紹完了再看這部分邏輯就比較容易理解了。 

InterpreterMacroAssembler::dispatch_next()函數中調用的dispatch_base()函數的實現如下:

void InterpreterMacroAssembler::dispatch_base(
  TosState  state, // 表示棧頂快取狀態
  address*  table,
  bool verifyoop
) {
  // ...
  // 獲取當前棧頂狀態位元組碼轉發表的地址,保存到rscratch1
  lea(rscratch1, ExternalAddress((address)table));
  // 跳轉到位元組碼對應的入口執行機器碼指令
  // address = rscratch1 + rbx * 8
  jmp(Address(rscratch1, rbx, Address::times_8));
} 

比如取一個位元組大小的指令(如iconst_0、aload_0等都是一個位元組大小的指令),那麼InterpreterMacroAssembler::dispatch_next()函數生成的彙編程式碼如下 :

// 在generate_fixed_frame()函數中
// 已經讓%r13存儲了bcp
// %ebx中存儲的是位元組碼的Opcode,也就是操作碼
movzbl 0x0(%r13),%ebx  
 
// $0x7ffff73ba4a0這個地址指向的
// 是對應state狀態下的一維數組,長度為256
movabs $0x7ffff73ba4a0,%r10

// 注意%r10中存儲的是常量,根據計算公式
// %r10+%rbx*8來獲取指向存儲入口地址的地址,
// 通過*(%r10+%rbx*8)獲取到入口地址,
// 然後跳轉到入口地址執行
jmpq *(%r10,%rbx,8)

%r10指向的是對應棧頂快取狀態state下的一維數組,長度為256,其中存儲的值為opcode,如下圖所示。

下面的函數顯示了對每個位元組碼的每個棧頂狀態都設置入口地址。

void DispatchTable::set_entry(int i, EntryPoint& entry) {
  assert(0 <= i && i < length, "index out of bounds");
  assert(number_of_states == 9, "check the code below");
  _table[btos][i] = entry.entry(btos);
  _table[ctos][i] = entry.entry(ctos);
  _table[stos][i] = entry.entry(stos);
  _table[atos][i] = entry.entry(atos);
  _table[itos][i] = entry.entry(itos);
  _table[ltos][i] = entry.entry(ltos);
  _table[ftos][i] = entry.entry(ftos);
  _table[dtos][i] = entry.entry(dtos);
  _table[vtos][i] = entry.entry(vtos);
}

其中的參數i就是opcode,各個位元組碼及對應的opcode可參考//docs.oracle.com/javase/specs/jvms/se8/html/index.html

所以_table表如下圖所示。

_table的一維為棧頂快取狀態,二維為Opcode,通過這2個維度能夠找到一段機器指令,這就是根據當前的棧頂快取狀態定位到的位元組碼需要執行的機器指令片段。 

調用dispatch_next()函數執行Java方法的位元組碼,其實就是根據位元組碼找到對應的機器指令片段的入口地址來執行,這段機器碼就是根據對應的位元組碼語義翻譯過來的,這些都會在後面詳細介紹。

 

推薦閱讀:

第1篇-關於JVM運行時,開篇說的簡單些

第2篇-JVM虛擬機這樣來調用Java主類的main()方法

第3篇-CallStub新棧幀的創建

第4篇-JVM終於開始調用Java主類的main()方法啦

第5篇-調用Java方法後彈出棧幀及處理返回結果

第6篇-Java方法新棧幀的創建

第7篇-為Java方法創建棧幀

如果有問題可直接評論留言或加作者微信mazhimazh

關注公眾號,有HotSpot源碼剖析系列文章!