1. 方法調用模塊入口
Java所有的方法調用都會經過JavaCalls模塊。該模塊又細分為call_virtual調用虛函數,call_static調用靜態函數等。虛函數調用會根據對象類型進行方法決議,所以需要獲取對象引用再查找實際要調用的方法;而靜態方法調用直接查找要調用的方法即可。不管怎樣,這些方法都是先找到要調用的方法methodHandle,然后傳給JavaCalls::call_helper()做實際的調用。
2. 尋找調用方法
現在我們知道了methodHandle表示實際要調用的方法,methodHandle里面有一個指向當前線程的指針,還有一個指向Method
類的指針,Method
位於hotspot\share\oops\method.hpp
,各種各樣的數據比如方法的訪問標志,內聯標志,用於編譯優化的計數等都落地於此。它的每個屬性的意義都是肉眼可見的重要:
_constMethod
指向方法中一些常量數據,比如常量池,max_local,max_stack,返回類型,參數個數,編譯-解釋適配器...這些參數的重要性不言而喻。
_method_data
存放一些計數信息和Profiling信息,比如方法重編譯了多少次,非逃逸參數有多少個,回邊有多少,有多少循環和基本塊。這些參數會影響后面的編譯器優化。
_method_counters
大量編譯優化相關的計數:
- 解釋器調用次數
- 解釋執行時由於異常而終止的次數
- 方法調用次數(method里面有多少方法調用)
- 回邊個數
- 該方法曾經過的分層編譯的最高層級
- 熱點方法計數
_access_flag
flag | 值 | 說明 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否為public |
ACC_PRIVATE | 0x0002 | 方法是否為private |
ACC_PROTECTED | 0x0004 | 方法是否為protected |
ACC_STATIC | 0x0008 | 方法是否為static |
ACC_FINAL | 0x0010 | 方法是否不可重寫 |
ACC_SYNCHRONIZED | 0x0020 | 是否存在方法鎖 |
ACC_BRIDGE | 0x0040 | 該方法是否由編譯器生成 |
ACC_VARARGS | 0x0080 | 是否存在可變參數 |
ACC_NATIVE | 0x0100 | 是否為native方法 |
ACC_ABSTRACT | 0x0400 | 是否為抽象方法 |
ACC_STRICT | 0x0800 | 是否啟用嚴格浮點模式 |
ACC_SYNTHETIC | 0x1000 | 是否是源代碼里面不存在的合成方法 |
_vtable_index
flag | 值 | 說明 |
---|---|---|
itable_index_max | -10 | 首個itable索引 |
pending_itable_index | -9 | itable將會被賦值 |
invalid_vtable_index | -4 | 無效虛表index |
garbage_vtable_index | -3 | 還沒有初始化vtable的方法,垃圾值 |
nonvirtual_vtable_index | -2 | 不需要虛函數派發,比如static函數就是這種 |
_flags
這個_flag不同於前面的_access_flag,它是表示這個方法具有什么特征,比如是否強制內聯,是否有@CallerSentitive注解,是否是有@HotSpotIntrinsicCandidate注解等
_intrinsic_id
固有方法(intrinsic method)在虛擬機中表示一些眾所周知的方法,針對它們可以做特設處理,生成獨特的代碼例程,虛擬機發現一個方法是固有方法就不會走逐行解釋字節碼這條路徑而是跳到獨特的代碼例程上面,所有的固有方法都定義在hotspot\share\classfile\vmSymbols.hpp
中,有興趣的可以去看看。
_compiled_invocation_count
編譯后的方法叫nmethod,這個就是用來計數編譯后的nmethod調用了多少次,如果該方法是解釋執行就為0。
_code
指向編譯后的本地代碼。
_from_interpreter_entry
解釋器入口,這個非常重要。之前提到JavaCalls::call得到methodHandle傳給call_helper做實際調用,call_helper會使用這個入口進入解釋器的世界。
_from_compiled_entry
如果該方法已經經過了編譯,那么就會使用該入口執行編譯后的代碼。
虛擬機是解釋編譯混合執行的模型,一個方法可能A時刻是解釋模式,B時刻是編譯模式,這就要求兩個入口都能進入正確的地方。hotspot使用一個適配器完成解釋編譯模式的切換:
之所以要加一個適配器是因為編譯產出的本地代碼用寄存器存放參數,解釋器用棧存放參數,適配器可以消除這些不同,同時正確設置入口點。
3. 建立棧幀
前面說道找到methodHandle后傳給call_helper做調用。其實,嚴格來說,call_helper還沒有做方法調用,它只是檢查了下方法是否需要進行編譯,驗證了參數等等,最終它是調用函數指針_call_stub_entry
,把方法調用這件事又轉交給了_call_stub_entry。
// hotspot\share\runtime\javaCalls.cpp
void JavaCalls::call_helper(JavaValue* result, const methodHandle& method, JavaCallArguments* args, TRAPS) {
...
// 調用函數指針_call_stub_entry,把實際的函數調用工作轉交給它。
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
StubRoutines::call_stub()(
(address)&link,
result_val_address,
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
result = link.result();
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
}
}
_call_stub_entry由generate_call_stub()生成,當調用Java方法前需要建立棧幀,該棧幀就是於此建立的。
另外StubRoutines::call_stub()()
是將_call_stub_entry強制類型轉換為指針然后執行,調試的時候不能對應源碼。如果使用Microsoft Visual Studio系列編譯器,點擊菜單欄調試->窗口->反匯編
:
然后在反匯編窗口STEP INTO進入call
:
在右方可以看到generate_call_stub()生成的機器碼(的匯編表示)了。由於generate_call_stub太多,這里就不逐行對照,請自行對應源碼和反匯編窗口的輸出,generate_call_stub里面是用匯編形式寫的機器碼生成,全部貼出來既無必要也沒意思,所以用注釋代替了,只保留最重要的邏輯:
// hotspot\cpu\x86\stubGenerator_x86_32.cpp
address generate_call_stub(address& return_address) {
// 保存重要的參數比如解釋器入口點,Java方法返回地址等
// 將Java方法的參數壓入棧
// 調用Java方法
__ movptr(rbx, method); // 將Method*指針存放到rbx
__ movptr(rax, entry_point); // 將解釋器入口存放到rax
__ mov(rsi, rsp); // 將當前棧頂存放到rsi
__ call(rax); // 進入解釋器入口!
// 處理Java方法返回值
// 彈出Java參數
// 返回
return start;
}
它首先建立了一個棧幀,這個棧幀里面保存了一些重要的數據,再把Java方法的參數壓入棧,當這一步完成,棧幀變成了這個樣子:
4. Java方法調用
當棧幀建立完畢就可以調用Java方法了。重復一次,Java方法調用使用如下代碼:
// 調用Java方法
__ movptr(rbx, method); // 將Method*指針存放到rbx
__ movptr(rax, entry_point); // 將解釋器入口存放到rax
__ mov(rsi, rsp); // 將當前棧頂存放到rsi
__ call(rax); // 進入解釋器入口!
前面三句將重要的數據放入寄存器,然后call rax
相當於call entry_point
,這個entry_point即解釋器入口點,最終的方法執行過程其實是在這里面的,_call_stub_entry只是一個樁代碼(Stub code),創建了棧幀,處理調用返回,實際的調用還是要跳到解釋器里面的。
樁代碼的意義有很多,常見的就是它是一個符合要求的簽名的函數,但是函數現在還沒有完全實現,那就留一個樁占位。比如一個系統需要讀取外部溫度:
void work(){
float temperature = readTemperatureFromSensor();
if(temperature>40.0){
...
}
}
float readTemperatureFromSensor(){
return 42.0f;
}
這個讀溫度的函數比較復雜,涉及傳感器的硬件編程,現階段我們只想完成外部即work的邏輯,那么就將readTemperatureFromSensor()做為一個stub,寫一個假的實現,后面再補全。
回到主題,虛擬機_call_stub_entry樁代碼的意思是它不完成具體任務(方法調用),只是做一些輔助工作(建立棧幀),而是跳到(call rax)解釋器入口完成具體任務,虛擬機中還有很多這樣的模式,其它叫法還有trampoline(跳床),以后都會遇到。
5. 總結
學而不思則罔,思而不學則殆。我們大概清楚了Java方法調用的流程,現在可以試着來總結一下:
JavaCalls里面的call_static()
或者call_virtual
通過方法決議找到要調用的方法methodHandle,傳遞給JavaCalls::call();JavaCalls::call()做一些簡單的檢查,比如方法是否需要進行C1/C2 JIT,參數對不對,之后調用_call_stub_entry,它會建立棧幀,進入解釋器執行字節碼,最后從解釋器返回,處理返回值,完成方法調用。詳細的調用棧如下:
JavaCalls::call_static() // 找到要調用的方法
-> JavaCalls::call()
-> os::os_exception_wrapper()
-> JavaCalls::call_helper()
-> _call_stub_entry() // 建立棧幀,處理解釋器返回值
-> `call rbx` // 進入解釋器入口點
附錄1. 使用hsdis查看對應的匯編表示
如果覺得上述調試方法過於麻煩,還有備選方案。下載hsdis-amd64.dll,將它放在jdk/bin/server/
目錄下,然后虛擬機加上參數-XX:+UnlockDiagnosticVMOptions -XX:+PrintStubCode
還可以查看上面的生成的機器代碼的匯編形式,不過除了驗證比對外一般人也很難從這大段匯編中看出什么...:
StubRoutines::call_stub [0x000001a53eb80b0c, 0x000001a53eb80efe[ (1010 bytes)
0x000001a53eb80b0c: push %rbp
0x000001a53eb80b0d: mov %rsp,%rbp
0x000001a53eb80b10: sub $0x1d8,%rsp
0x000001a53eb80b17: mov %r9,0x28(%rbp)
0x000001a53eb80b1b: mov %r8d,0x20(%rbp)
0x000001a53eb80b1f: mov %rdx,0x18(%rbp)
0x000001a53eb80b23: mov %rcx,0x10(%rbp)
0x000001a53eb80b27: mov %rbx,-0x8(%rbp)
0x000001a53eb80b2b: mov %r12,-0x20(%rbp)
0x000001a53eb80b2f: mov %r13,-0x28(%rbp)
0x000001a53eb80b33: mov %r14,-0x30(%rbp)
0x000001a53eb80b37: mov %r15,-0x38(%rbp)
0x000001a53eb80b3b: vmovdqu %xmm6,-0x48(%rbp)
0x000001a53eb80b40: vmovdqu %xmm7,-0x58(%rbp)
0x000001a53eb80b45: vmovdqu %xmm8,-0x68(%rbp)
0x000001a53eb80b4a: vmovdqu %xmm9,-0x78(%rbp)
0x000001a53eb80b4f: vmovdqu %xmm10,-0x88(%rbp)
0x000001a53eb80b57: vmovdqu %xmm11,-0x98(%rbp)
0x000001a53eb80b5f: vmovdqu %xmm12,-0xa8(%rbp)
0x000001a53eb80b67: vmovdqu %xmm13,-0xb8(%rbp)
0x000001a53eb80b6f: vmovdqu %xmm14,-0xc8(%rbp)
0x000001a53eb80b77: vmovdqu %xmm15,-0xd8(%rbp)
; 省略500+行
附錄2. 解釋器入口點
意猶未盡嗎?上面省略了很多東西,比如進入解釋器入口點執行字節碼這個重要的事情。那么解釋器入口點在哪?我們知道解釋器是在虛擬機創建的時候JIT生成的,可以跟蹤虛擬機創建找到它,它的調用棧如下:
Threads::create_vm()
-> init_globals()
-> interpreter_init()()
-> TemplateInterpreter::initialize()
-> TemplateInterpreterGenerator() // 構造函數
-> TemplateInterpreterGenerator::generate_all()
-> TemplateInterpreterGenerator::generate_normal_entry()
普通方法(非synchronized,非native)的解釋器入口點是通過\hotspot\cpu\x86\templateInterpreterGenerator_x86.cpp
中的generate_normal_entry()生成的。
附錄3. 設置解釋器入口點
還是這個問題,我們知道了解釋器入口點在哪,但是這個解釋器入口點又是怎么和方法關聯起來的呢?
Java的類在虛擬機中會經過加載 -> 鏈接 -> 初始化 三個步驟,網上有很多詳細解釋這里就不在贅述。具體來說instanceKlass
在虛擬機中表示一個Java類,它使用instanceKlass::link_class()
做鏈接過程。類的鏈接會觸發類中方法的Method::link_method()
,它會給方法設置正確的解釋器入口點,編譯器適配器等:
// hotspot\share\oops\method.cpp
void Method::link_method(const methodHandle& h_method, TRAPS) {
...
if (!is_shared()) {
// entry_for_method會找到剛剛generate_normal_entry設置的入口點
address entry = Interpreter::entry_for_method(h_method);
// 將它設置為解釋器入口點,即可_i2i_entry和_from_interpreted_entry
set_interpreter_entry(entry);
}
...
// 設置_from_compiled_entry的適配器
(void) make_adapters(h_method, CHECK);
}