在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可參考https://docs.oracle.com/javase/specs/jvms/se8/html/index.html。
所以_table表如下圖所示。
_table的一維為棧頂緩存狀態,二維為Opcode,通過這2個維度能夠找到一段機器指令,這就是根據當前的棧頂緩存狀態定位到的字節碼需要執行的機器指令片段。
調用dispatch_next()函數執行Java方法的字節碼,其實就是根據字節碼找到對應的機器指令片段的入口地址來執行,這段機器碼就是根據對應的字節碼語義翻譯過來的,這些都會在后面詳細介紹。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流