在 第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類中定義的一個屬性,如上方法直接返回了這個屬性的值。那麼這個屬性是何時賦值的?其實是在方法連線(也就是在類的生命週期中的類連線階段會進行方法連線)時會設定。方法連線時會呼叫如下方法:

// Called when the method_holder is getting linked.
// Setup entrypoints so the method
// is ready to be called from interpreter,
// compiler, and vtables.
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()函式時用到,所以我們需要在這裡強調一下。

推薦閱讀:

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

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

第3篇-CallStub新棧幀的建立

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

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

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

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