在前一篇 第1篇-關於JVM執行時,開篇說的簡單些 中介紹了call_static()、call_virtual()等函式的作用,這些函式會呼叫JavaCalls::call()函式。我們看Java類中main()方法的呼叫,呼叫棧如下:
JavaCalls::call_helper() at javaCalls.cpp
os::os_exception_wrapper() at os_linux.cpp
JavaCalls::call() at javaCalls.cpp
jni_invoke_static() at jni.cpp
jni_CallStaticVoidMethod() at jni.cpp
JavaMain() at java.c
start_thread() at pthread_create.c
clone() at clone.S
這是Linux上的呼叫棧,通過JavaCalls::call_helper()函式來執行main()方法。棧的起始函式為clone(),這個函式會為每個程序(Linux程序對應著Java執行緒)建立單獨的棧空間,這個棧空間如下圖所示。
在Linux作業系統上,棧的地址向低地址延伸,所以未使用的棧空間在已使用的棧空間之下。圖中的每個藍色小格表示對應方法的棧幀,而棧就是由一個一個的棧幀組成。native方法的棧幀、Java解釋棧幀和Java編譯棧幀都會在黃色區域中分配,所以說他們寄生在宿主棧中,這些不同的棧幀都緊密的挨在一起,所以並不會產生什麼空間碎片這類的問題,而且這樣的佈局非常有利於進行棧的遍歷。上面給出的呼叫棧就是通過遍歷一個一個棧幀得到的,遍歷過程也是棧展開的過程。後續對於異常的處理、執行jstack列印執行緒堆疊、GC查詢根引用等都會對棧進行展開操作,所以棧展開是後面必須要介紹的。
下面我們繼續看JavaCalls::call_helper()函式,這個函式中有個非常重要的呼叫,如下:
// do call
{
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(); // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
// Preserve oop return value across possible gc points
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
} // Exit JavaCallWrapper (can block - potential return oop must be preserved)
呼叫StubRoutines::call_stub()函式返回一個函式指標,然後通過函式指標來呼叫函式指標指向的函式。通過函式指標呼叫和通過函式名呼叫的方式一樣,這裡我們需要清楚的是,呼叫的目標函式仍然是C/C++函式,所以由C/C++函式呼叫另外一個C/C++函式時,要遵守呼叫約定。這個呼叫約定會規定怎麼給被呼叫函式(Callee)傳遞引數,以及被呼叫函式的返回值將儲存在什麼地方。
下面我們就來簡單說說Linux X86架構下的C/C++函式呼叫約定,在這個約定下,以下暫存器用於傳遞引數:
- 第1個引數:rdi c_rarg0
- 第2個引數:rsi c_rarg1
- 第3個引數:rdx c_rarg2
- 第4個引數:rcx c_rarg3
- 第5個引數:r8 c_rarg4
- 第6個引數:r9 c_rarg5
在函式呼叫時,6個及小於6個用如下暫存器來傳遞,在HotSpot中通過更易理解的別名c_rarg*來使用對應的暫存器。如果引數超過六個,那麼程式將會用呼叫棧來傳遞那些額外的引數。
數一下我們通過函式指標呼叫時傳遞了幾個引數?8個,那麼後面的2個就需要通過呼叫函式(Caller)的棧來傳遞,這兩個引數就是args->size_of_parameters()和CHECK(這是個巨集,擴充套件後就是傳遞執行緒物件)。
所以我們的呼叫棧在呼叫函式指標指向的函式時,變為了如下狀態:
右邊是具體的call_helper()棧幀中的內容,我們把thread和parameter size壓入了呼叫棧中,其實在調目標函式的過程還會開闢新的棧幀並在parameter size後壓入返回地址和呼叫棧的棧底,下一篇我們再詳細介紹。先來介紹下JavaCalls::call_helper()函式的實現,我們分3部分依次介紹。
1、檢查目標方法是否"首次執行前就必須被編譯”,是的話呼叫JIT編譯器去編譯目標方法;
程式碼實現如下:
void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
methodHandle method = *m;
JavaThread* thread = (JavaThread*)THREAD;
... assert(!thread->is_Compiler_thread(), "cannot compile from the compiler");
if (CompilationPolicy::must_be_compiled(method)) {
CompileBroker::compile_method(method, InvocationEntryBci,
CompilationPolicy::policy()->initial_compile_level(),
methodHandle(), 0, "must_be_compiled", CHECK);
}
...
}
對於main()方法來說,如果配置了-Xint選項,則是以解釋模式執行的,所以並不會走上面的compile_method()函式的邏輯。後續我們要研究編譯執行時,可以強制要求進行編譯執行,然後檢視執行過程。
2、獲取目標方法的解釋模式入口from_interpreted_entry,也就是entry_point的值。獲取的entry_point就是為Java方法呼叫準備棧楨,並把程式碼呼叫指標指向method的第一個位元組碼的記憶體地址。entry_point相當於是method的封裝,不同的method型別有不同的entry_point。
接著看call_helper()函式的程式碼實現,如下:
address entry_point = method->from_interpreted_entry();
呼叫method的from_interpreted_entry()函式獲取Method例項中_from_interpreted_entry屬性的值,這個值到底在哪裡設定的呢?我們後面會詳細介紹。
3、呼叫call_stub()函式,需要傳遞8個引數。這個程式碼在前面給出過,這裡不再給出。下面我們詳細介紹一下這幾個引數,如下:
(1)link 此變數的型別為JavaCallWrapper,這個變數對於棧展開過程非常重要,後面會詳細介紹;
(2)result_val_address 函式返回值地址;
(3)result_type 函式返回型別;
(4)method() 當前要執行的方法。通過此引數可以獲取到Java方法所有的元資料資訊,包括最重要的位元組碼資訊,這樣就可以根據位元組碼資訊解釋執行這個方法了;
(5)entry_point HotSpot每次在呼叫Java函式時,必然會呼叫CallStub函式指標,這個函式指標的值取自_call_stub_entry,HotSpot通過_call_stub_entry指向被呼叫函式地址。在呼叫函式之前,必須要先經過entry_point,HotSpot實際是通過entry_point從method()物件上拿到Java方法對應的第1個位元組碼命令,這也是整個函式的呼叫入口;
(6)args->parameters() 描述Java函式的入參資訊;
(7)args->size_of_parameters() 描述Java函式的入參的大小;
(8)CHECK 當前執行緒物件。
這裡最重要的就是entry_point了,這也是下一篇要介紹的內容。
搭建過程中如果有問題可直接評論留言或加作者微信mazhimazh。
關注公眾號,有HotSpot原始碼剖析系列文章!