JVM的方法執行引擎-entry point棧幀
- 2020 年 8 月 18 日
- 筆記
接著上一篇去講,回到JavaCalls::call_helper()中:
address entry_point = method->from_interpreted_entry();
entry_point是從當前要執行的Java方法中獲取的,定義如下:
源程式碼位置:/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.hpp中有這樣一個set方法:
void set_interpreter_entry(address entry) { _i2i_entry = entry; _from_interpreted_entry = entry; }
在連接方法時通過如下的方法調用上面的方法:
// 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); assert(entry != NULL, "interpreter entry must be non-null"); // Sets both _i2i_entry and _from_interpreted_entry set_interpreter_entry(entry); // ... }
根據注釋都可以得知,當方法連接時,會去設置方法的entry_point,entry_point是通過調用Interpreter::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地址。給數組中元素賦值專門有個方法:
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()去生成每種方法的entry_point,所有Java方法的執行,都會通過對應類型的entry_point常式來輔助。下面來詳細介紹一下generate_all()方法的實現邏輯。
HotSpot在啟動時,會為所有位元組碼創建在特定目標平台上運行的機器碼,並存放在CodeCache中,在解釋執行位元組碼的過程中,就會從CodeCache中取出這些本地機器碼並執行。
在啟動虛擬機階段會調用init_globals()方法初始化全局模組,在這個方法中通過調用interpreter_init()方法初始化模板解釋器,調用棧如下:
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
interpreter_init()方法主要是通過調用TemplateInterpreter::initialize()方法來完成邏輯,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); } // initialize dispatch table _active_table = _normal_table; }
模板解釋器的初始化包括如下幾個方面:
(1)抽象解釋器AbstractInterpreter的初始化,AbstractInterpreter是基於彙編模型的解釋器的共同基類,定義了解釋器和解釋器生成器的抽象介面。
(2)模板表TemplateTable的初始化,模板表TemplateTable保存了各個位元組碼的模板(目標程式碼生成函數和參數);
(3)CodeCache的Stub隊列StubQueue的初始化;
(4)解釋器生成器InterpreterGenerator的初始化。
在執行InterpreterGenerator g(_code)程式碼時,調用InterpreterGenerator的構造函數,如下:
InterpreterGenerator::InterpreterGenerator(StubQueue* code) : TemplateInterpreterGenerator(code) { generate_all(); // down here so it can be "virtual" }
調用的generate_all()方法將生成一系列HotSpot運行過程中所執行的一些公共程式碼的入口和所有位元組碼的InterpreterCodelet。這些入口包括:
- error exits:出錯退出處理入口
- 位元組碼追蹤入口(配置了-XX:+TraceBytecodes)
- 函數返回入口
- JVMTI的EarlyReturn入口
- 逆優化調用返回入口
- native調用返回值處理handlers入口
- continuation入口
- safepoint入口
- 異常處理入口
- 拋出異常入口
- 方法入口(native方法和非native方法)
- 位元組碼入口
部分重要的入口實現邏輯會在後面詳細介紹,這裡只看為非native方法入口(也就是普通的、沒有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是宏,擴展後如上的調用語句變為如下的形式:
Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);
_entry_table變數定義在AbstractInterpreter類中,如下:
// method entry points static address _entry_table[number_of_method_entries]; // entry points for a given method
number_of_method_entries表示方法類型的總數,使用方法類型做為數組下標就可以獲取對應的方法入口。調用generate_method_entry()方法為各個類型的方法生成對應的方法入口,實現如下:
address AbstractInterpreterGenerator::generate_method_entry(AbstractInterpreter::MethodKind kind) { // determine code generation flags bool synchronized = false; address entry_point = NULL; InterpreterGenerator* ig_this = (InterpreterGenerator*)this; switch (kind) { // 根據方法類型kind生成不同的入口 case Interpreter::zerolocals : // zerolocals表示普通方法類型 break; case Interpreter::zerolocals_synchronized: // zerolocals表示普通的、同步方法類型 synchronized = true; break; // ... } if (entry_point) { return entry_point; } return ig_this->generate_normal_entry(synchronized); }
zerolocals表示正常的Java方法調用(包括Java程式的主函數),對於zerolocals來說,會調用ig_this->generate_normal_entry()方法生成入口。generate_normal_entry()方法會為執行的方法生成堆棧,而堆棧由局部變數表(用來存儲傳入的參數和被調用函數的局部變數)、幀數據和操作數棧這三大部分組成,所以方法會創建這3部分來輔助Java方法的執行。
之前在介紹CallStub棧幀時講到過,如果要執行entry_point,那麼棧幀的狀態就如下圖所示。
/src/cpu/x86/vm/templateInterpreter_x86_64.cpp文件中generate_normal_entry()方法在通過CallStub調用時,各個暫存器的狀態如下:
rbx -> Method* r13 -> sender sp rsi -> entry point
generate_normal_entry()方法的實現如下:
// Generic interpreted method entry to (asm) interpreter address InterpreterGenerator::generate_normal_entry(bool synchronized) { // determine code generation flags bool inc_counter = UseCompiler || CountCompiledCalls; // 執行如下方法前的暫存器中保存的值如下: // ebx: Method* // r13: sender sp address entry_point = __ pc(); // entry_point函數的程式碼入口地址 // 當前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()); // 上面已經說明了獲取各種方法元數據的計算方式,但並沒有執行計算,下面會生成對應的彙編來執行計算 // get parameter size (always needed) __ movptr(rdx, constMethod); // 計算ConstMethod*,保存在rdx裡面 __ load_unsigned_short(rcx, size_of_parameters); // 計算parameter大小,保存在rcx裡面 //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); // see if we've got enough room on the stack for locals plus overhead. generate_stack_overflow_check(); //返回地址是在call_stub中保存的,如果不彈出堆棧到rax,那麼局部變數區就如下面的樣子: // [parameter 1] // [parameter 2] // ...... // [parameter n] // [return address] // [local 1] // [local 2] // ... // [local n] // 顯然中間有個return address使的局部變數表不是連續的,這會導致其中的局部變數計算方式不一致,所以暫時將返回地址存儲到rax中 // get return address __ pop(rax); // compute beginning of parameters (r14) // 計算第1個參數的地址:當前棧頂地址 + 變數大小 * 8 - 一個字大小。 // 這兒注意,因為地址保存在低地址上,而堆棧是向低地址擴展的,所以只需加n-1個變數大小就可以得到第1個參數的地址。 __ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize)); // 把函數的局部變數全置為0,也就是做初始化,防止之前遺留下的值影響 // rdx:被調用函數的局部變數可使用的大小 // allocate space for locals // explicitly initialize locals { Label exit, loop; __ testl(rdx, rdx); __ jcc(Assembler::lessEqual, exit); // do nothing if rdx <= 0 __ bind(loop); __ push((int) NULL_WORD); // initialize local variables __ decrementl(rdx); // until everything initialized __ jcc(Assembler::greater, loop); __ bind(exit); } // 生成固定楨 // initialize fixed part of activation frame generate_fixed_frame(false); // 省略統計及棧溢出等邏輯,後面會詳細介紹 // check for synchronized methods // Must happen AFTER invocation_counter check and stack overflow check, // so method is not locked if overflows. if (synchronized) { // Allocate monitor and lock method lock_method(); } else { // no synchronization necessary } // 跳轉到目標Java方法的第一條位元組碼指令,並執行其對應的機器指令 __ dispatch_next(vtos); // 省略統計相關邏輯,後面會詳細介紹 return entry_point; }
要對偏移的計算進行研究,如下:
// 當前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());
如果要列印這個方法生成的彙編程式碼,可以在方法的return語句之前添加如下2句列印程式碼:
address end = __ pc(); Disassembler::decode(entry_point, end);
這樣,在執行Disassembler::decode()方法時,會將此方法生成的機器碼轉換為彙編列印到控制台上。
調用generate_fixed_frame()方法之前生成的彙編程式碼如下:
Loaded disassembler from /home/mazhi/workspace/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/hsdis-amd64.so [Disassembling for mach='i386:x86-64'] 0x00007fffe101e2e0: mov 0x10(%rbx),%rdx // 通過%rbx中保存的Method*找到ConstMethod並保存到%rdx 0x00007fffe101e2e4: movzwl 0x2a(%rdx),%ecx // 通過ConstMethod*找到入參數量保存在%ecx 0x00007fffe101e2e8: movzwl 0x28(%rdx),%edx // 通過ConstMethod*找到本地變數表大小保存在%edx 0x00007fffe101e2ec: sub %ecx,%edx // 計算方法局部變數可使用的本地變數空間的大小並保存在%edx // ... 省略調用generate_stack_overflow_check()方法生成的彙編 0x00007fffe101e43d: pop %rax // 彈出返回地址 0x00007fffe101e43e: lea -0x8(%rsp,%rcx,8),%r14 // 計算第一個參數的地址 // 為局部變數slot(不包括方法入參)分配堆棧空間並初始化為0 // 循環進行本地變數表空間的開闢 // -- loop -- 0x00007fffe101e443: test %edx,%edx 0x00007fffe101e445: jle 0x00007fffe101e454 // 由於%edx的大小等於0,所以不需要額外分配,直接跳轉到exit 0x00007fffe101e44b: pushq $0x0 0x00007fffe101e450: dec %edx 0x00007fffe101e452: jg 0x00007fffe101e44b // 如果%edx的大小不等於0,跳轉到loop
現在棧的狀態如下圖所示。
現在r14指向局部變數開始的位置,而argument和local variable都存儲在了局部變數表,rbp指向了局部變數表結束位置。現在各個暫存器的狀態如下:
rax: return address // %rax暫存器中存儲的是返回地址return address rbx: Method* r14: pointer to locals r13: sender sp
在InterpreterGenerator::generate_normal_entry()函數中,接下來會以這樣的狀態調用generate_fixed_frame()函數來創建Java方法運行時所需要的棧幀。generate_fixed_frame()函數會在下一篇詳細介紹。
調用後棧幀變為如下的狀態:
上圖右邊的棧狀態隨著具體方法的不同會顯示不同的狀態,不過大概的狀態就是上圖所示的樣子。
調用完generate_fixed_frame()方法後一些暫存器中保存的值如下:
rbx:Method* ecx:invocation counter r13:bcp(byte code pointer) rdx:ConstantPool* 常量池的地址 r14:本地變數表第1個參數的地址
執行完generate_fixed_frame()方法後會繼續執行InterpreterGenerator::generate_normal_entry()函數,如果是為同步方法生成機器碼,那麼還需要調用lock_method()方法,這個方法會改變當前棧的狀態,添加同步所需要的一些資訊,在後面介紹鎖的實現時會詳細介紹。
InterpreterGenerator::generate_normal_entry()函數最終會返回生成機器碼的入口執行地址,然後通過變數_entry_table數組來保存,這樣就可以使用方法類型做為數組下標獲取對應的方法入口了。
相關文章的鏈接如下:
1、在Ubuntu 16.04上編譯OpenJDK8的源程式碼
13、類載入器
14、類的雙親委派機制
15、核心類的預裝載
16、Java主類的裝載
17、觸發類的裝載
18、類文件介紹
19、文件流
20、解析Class文件
21、常量池解析(1)
22、常量池解析(2)
23、欄位解析(1)
24、欄位解析之偽共享(2)
25、欄位解析(3)
28、方法解析
29、klassVtable與klassItable類的介紹
30、計算vtable的大小
31、計算itable的大小
32、解析Class文件之創建InstanceKlass對象
33、欄位解析之欄位注入
34、類的連接
35、類的連接之驗證
36、類的連接之重寫(1)
37、類的連接之重寫(2)
38、方法的連接
39、初始化vtable
40、初始化itable
41、類的初始化
42、對象的創建
43、Java引用類型
作者持續維護的個人部落格 classloading.com。
關注公眾號,有HotSpot源碼剖析系列文章!