在 第2篇-JVM虛擬機這樣來調用Java主類的main()方法 介紹JavaCalls::call_helper()函數的實現時提到過如下一句代碼:
address entry_point = method->from_interpreted_entry();
這個參數會做為實參傳遞給StubRoutines::call_stub()函數指針指向的“函數”,然后在 第4篇-JVM終於開始調用Java主類的main()方法啦 介紹到通過callq指令調用entry_point,那么這個entry_point到底是什么呢?這一篇我們將詳細介紹。
首先看from_interpreted_entry()函數實現,如下:
源代碼位置:/openjdk/hotspot/src/share/vm/oops/method.hpp
volatile address from_interpreted_entry() const{
return (address)OrderAccess::load_ptr_acquire(&_from_interpreted_entry);
}
_from_interpreted_entry只是Method類中定義的一個屬性,如上方法直接返回了這個屬性的值。那么這個屬性是何時賦值的?其實是在方法連接(也就是在類的生命周期中的類連接階段會進行方法連接)時會設置。方法連接時會調用如下方法:
void Method::link_method(methodHandle h_method, TRAPS) {
// ...
address entry = Interpreter::entry_for_method(h_method);
// Sets both _i2i_entry and _from_interpreted_entry
set_interpreter_entry(entry);
// ...
}
首先調用Interpreter::entry_for_method()函數根據特定方法類型獲取到方法的入口,得到入口entry后會調用set_interpreter_entry()函數將值保存到對應屬性上。set_interpreter_entry()函數的實現非常簡單,如下:
void set_interpreter_entry(address entry) {
_i2i_entry = entry;
_from_interpreted_entry = entry;
}
可以看到為_from_interpreted_entry屬性設置了entry值。
下面看一下entry_for_method()函數的實現,如下:
static address entry_for_method(methodHandle m) {
return entry_for_kind(method_kind(m));
}
首先通過method_kind()函數拿到方法對應的類型,然后調用entry_for_kind()函數根據方法類型獲取方法對應的入口entry_point。調用的entry_for_kind()函數的實現如下:
static address entry_for_kind(MethodKind k){
return _entry_table[k];
}
這里直接返回了_entry_table數組中對應方法類型的entry_point地址。
這里涉及到Java方法的類型MethodKind,由於要通過entry_point進入Java世界,執行Java方法相關的邏輯,所以entry_point中一定會為對應的Java方法建立新的棧幀,但是不同方法的棧幀其實是有差別的,如Java普通方法、Java同步方法、有native關鍵字的Java方法等,所以就把所有的方法進行了歸類,不同類型獲取到不同的entry_point入口。到底有哪些類型,我們可以看一下MethodKind這個枚舉類中定義出的枚舉常量:
enum MethodKind {
zerolocals, // 普通的方法
zerolocals_synchronized, // 普通的同步方法
native, // native方法
native_synchronized, // native同步方法
...
}
當然還有其它一些類型,不過最主要的就是如上枚舉類中定義出的4種類型方法。
為了能盡快找到某個Java方法對應的entry_point入口,把這種對應關系保存到了_entry_table中,所以entry_for_kind()函數才能快速的獲取到方法對應的entry_point入口。 給數組中元素賦值專門有個方法:
void AbstractInterpreter::set_entry_for_kind(AbstractInterpreter::MethodKind kind, address entry) {
_entry_table[kind] = entry;
}
那么何時會調用set_entry_for_kind()函數呢,答案就在TemplateInterpreterGenerator::generate_all()函數中,generate_all()函數會調用generate_method_entry()函數生成每種Java方法的entry_point,每生成一個對應方法類型的entry_point就保存到_entry_table中。
下面詳細介紹一下generate_all()函數的實現邏輯,在HotSpot啟動時就會調用這個函數生成各種Java方法的entry_point。調用棧如下:
TemplateInterpreterGenerator::generate_all() templateInterpreter.cpp
InterpreterGenerator::InterpreterGenerator() templateInterpreter_x86_64.cpp
TemplateInterpreter::initialize() templateInterpreter.cpp
interpreter_init() interpreter.cpp
init_globals() init.cpp
Threads::create_vm() thread.cpp
JNI_CreateJavaVM() jni.cpp
InitializeJVM() java.c
JavaMain() java.c
start_thread() pthread_create.c
調用的generate_all()函數將生成一系列HotSpot運行過程中所執行的一些公共代碼的入口和所有字節碼的InterpreterCodelet,一些非常重要的入口實現邏輯會在后面詳細介紹,這里只看普通的、沒有native關鍵字修飾的Java方法生成入口的邏輯。generate_all()函數中有如下實現:
#define method_entry(kind) \
{ \
CodeletMark cm(_masm, "method entry point (kind = " #kind ")"); \
Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind); \
}
method_entry(zerolocals)
其中method_entry是一個宏,擴展后如上的method_entry(zerolocals)語句變為如下的形式:
Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);
_entry_table變量定義在AbstractInterpreter類中,如下:
static address _entry_table[number_of_method_entries];
number_of_method_entries表示方法類型的總數,使用方法類型做為數組下標就可以獲取對應的方法入口。調用generate_method_entry()函數為各種類型的方法生成對應的方法入口。generate_method_entry()函數的實現如下:
address AbstractInterpreterGenerator::generate_method_entry(AbstractInterpreter::MethodKind kind) {
bool synchronized = false;
address entry_point = NULL;
InterpreterGenerator* ig_this = (InterpreterGenerator*)this;
// 根據方法類型kind生成不同的入口
switch (kind) {
// 表示普通方法類型
case Interpreter::zerolocals :
break;
// 表示普通的、同步方法類型
case Interpreter::zerolocals_synchronized:
synchronized = true;
break;
// ...
}
if (entry_point) {
return entry_point;
}
return ig_this->generate_normal_entry(synchronized);
}
zerolocals表示正常的Java方法調用,包括Java程序的main()方法,對於zerolocals來說,會調用ig_this->generate_normal_entry()函數生成入口。generate_normal_entry()函數會為執行的方法生成堆棧,而堆棧由局部變量表(用來存儲傳入的參數和被調用方法的局部變量)、Java方法棧幀數據和操作數棧這三大部分組成,所以entry_point例程(其實就是一段機器指令片段,英文名為stub)會創建這3部分來輔助Java方法的執行。
我們還是回到開篇介紹的知識點,通過callq指令調用entry_point例程。此時的棧幀狀態在 第4篇-JVM終於開始調用Java主類的main()方法啦 中介紹過,為了大家閱讀的方便,這里再次給出:
注意,在執行callq指令時,會將函數的返回地址存儲到棧頂,所以上圖中會壓入return address一項。
CallStub()函數在通過callq指令調用generate_normal_entry()函數生成的entry_point時,有幾個寄存器中存儲着重要的值,如下:
rbx -> Method*
r13 -> sender sp
rsi -> entry point
下面就是分析generate_normal_entry()函數的實現邏輯了,這是調用Java方法的最重要的部分。函數的重要實現邏輯如下:
address InterpreterGenerator::generate_normal_entry(bool synchronized) {
// ...
// entry_point函數的代碼入口地址
address entry_point = __ pc();
// 當前rbx中存儲的是指向Method的指針,通過Method*找到ConstMethod*
const Address constMethod(rbx, Method::const_offset());
// 通過Method*找到AccessFlags
const Address access_flags(rbx, Method::access_flags_offset());
// 通過ConstMethod*得到parameter的大小
const Address size_of_parameters(rdx,ConstMethod::size_of_parameters_offset());
// 通過ConstMethod*得到local變量的大小
const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset());
// 上面已經說明了獲取各種方法元數據的計算方式,
// 但並沒有執行計算,下面會生成對應的匯編來執行計算
// 計算ConstMethod*,保存在rdx里面
__ movptr(rdx, constMethod);
// 計算parameter大小,保存在rcx里面
__ load_unsigned_short(rcx, size_of_parameters);
// rbx:保存基址;rcx:保存循環變量;rdx:保存目標地址;rax:保存返回地址(下面用到)
// 此時的各個寄存器中的值如下:
// rbx: Method*
// rcx: size of parameters
// r13: sender_sp (could differ from sp+wordSize
// if we were called via c2i ) 即調用者的棧頂地址
// 計算local變量的大小,保存到rdx
__ load_unsigned_short(rdx, size_of_locals);
// 由於局部變量表用來存儲傳入的參數和被調用方法的局部變量,
// 所以rdx減去rcx后就是被調用方法的局部變量可使用的大小
__ subl(rdx, rcx);
// ...
// 返回地址是在CallStub中保存的,如果不彈出堆棧到rax,中間
// 會有個return address使的局部變量表不是連續的,
// 這會導致其中的局部變量計算方式不一致,所以暫時將返
// 回地址存儲到rax中
__ pop(rax);
// 計算第1個參數的地址:當前棧頂地址 + 變量大小 * 8 - 一個字大小
// 注意,因為地址保存在低地址上,而堆棧是向低地址擴展的,所以只
// 需加n-1個變量大小就可以得到第1個參數的地址
__ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize));
// 把函數的局部變量設置為0,也就是做初始化,防止之前遺留下的值影響
// rdx:被調用方法的局部變量可使用的大小
{
Label exit, loop;
__ testl(rdx, rdx);
// 如果rdx<=0,不做任何操作
__ jcc(Assembler::lessEqual, exit);
__ bind(loop);
// 初始化局部變量
__ push((int) NULL_WORD);
__ decrementl(rdx);
__ jcc(Assembler::greater, loop);
__ bind(exit);
}
// 生成固定楨
generate_fixed_frame(false);
// ... 省略統計及棧溢出等邏輯,后面會詳細介紹
// 如果是同步方法時,還需要執行lock_method()函數,所以
// 會影響到棧幀布局
if (synchronized) {
// Allocate monitor and lock method
lock_method();
}
// 跳轉到目標Java方法的第一條字節碼指令,並執行其對應的機器指令
__ dispatch_next(vtos);
// ... 省略統計相關邏輯,后面會詳細介紹
return entry_point;
}
這個函數的實現看起來比較多,但其實邏輯實現比較簡單,就是根據被調用方法的實際情況創建出對應的局部變量表,然后就是2個非常重要的函數generate_fixed_frame()和dispatch_next()函數了,這2個函數我們后面再詳細介紹。
在調用generate_fixed_frame()函數之前,棧的狀態變為了下圖所示的狀態。
與前一個圖對比一下,可以看到多了一些local variable 1 ... local variable n等slot,這些slot與argument word 1 ... argument word n共同構成了被調用的Java方法的局部變量表,也就是圖中紫色的部分。其實local variable 1 ... local variable n等slot屬於被調用的Java方法棧幀的一部分,而argument word 1 ... argument word n卻屬於CallStub()函數棧幀的一部分,這2部分共同構成局部變量表,專業術語叫棧幀重疊。
另外還能看出來,%r14指向了局部變量表的第1個參數,而CallStub()函數的return address被保存到了%rax中,另外%rbx中依然存儲着Method*。這些寄存器中保存的值將在調用generate_fixed_frame()函數時用到,所以我們需要在這里強調一下。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流