接着上一篇去講,回到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
由於調用generate_call_stub()函數生成CallStub,在generate_call_stub()函數中會調用generate_normal_entry()函數時需要保存下這個地址,這個地址就存在rax中。
在InterpreterGenerator::generate_normal_entry()函數中,接下來會以這樣的狀態調用generate_fixed_frame()函數來創建Java方法運行時所需要的棧幀。generate_fixed_frame()函數會在下一篇詳細介紹。
調用后棧幀變為如下的狀態:
上圖藍色的棧狀態隨着具體方法的不同會顯示不同的狀態,不過大概的狀態就是上圖所示的樣子。其中的last sp的作用如下:
An i2c adapter is frameless because the *caller* frame, which is interpreted,routinely 常規地; 通常地; repairs its own stack pointer (from interpreter_frame_last_sp), 這個對理解last_sp非常重要 even if a callee has modified the stack pointer.
對於sender sp的作用如下:
A c2i adapter is frameless because the *callee* frame, which is interpreted,routinely 常規地; 通常地; repairs its caller's stack pointer (from sender_sp, which is set up via the senderSP register).
old stack pointer(sender_sp)在被調用者棧幀中,存儲調用者在跳轉時的 sp 指針。
last sp 在調用者棧幀中,存儲調用者跳轉時的 sp 指針(僅限 java 函數間跳轉)。
調用者棧幀中的 last sp 和被調用者棧幀中的 sender sp 指向同一位置。
調用完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個第2篇文章的主要函數調用,如下圖所示。
相關文章的鏈接如下:
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源碼剖析系列文章!