開講Java執行時,這一篇講一些簡單的內容。我們寫的主類中的main()方法是如何被Java虛擬機器呼叫到的?在Java類中的一些方法會被由C/C++編寫的HotSpot虛擬機器的C/C++函式呼叫,不過由於Java方法與C/C++函式的呼叫約定不同,所以並不能直接呼叫,需要JavaCalls::call()這個函式輔助呼叫。(我把由C/C++編寫的叫函式,把Java編寫的叫方法,後續也會延用這樣的叫法)如下圖所示。
從C/C++函式中呼叫的一些Java方法主要有:
(1)Java主類中的main()方法;
(2)Java主類裝載時,呼叫JavaCalls::call()函式執行checkAndLoadMain()方法;
(3)類的初始化過程中,呼叫JavaCalls::call()函式執行的Java類初始化方法<clinit>,可以檢視JavaCalls::call_default_constructor()函式,有對<clinit>方法的呼叫邏輯;
(4)我們先省略main方法的執行流程(其實main方法的執行也是先啟動一個JavaMain執行緒,套路都是一樣的),單看某個JavaThread的啟動過程。JavaThread的啟動最終都要通過一個native方法java.lang.Thread#start0()方法完成的,這個方法經過直譯器的native_entry入口,呼叫到了JVM_StartThread()函式。其中的static void thread_entry(JavaThread* thread, TRAPS)函式中會呼叫JavaCalls::call_virtual()函式。JavaThread最終會通過JavaCalls::call_virtual()函式來呼叫位元組碼中的run()方法;
(5)在SystemDictionary::load_instance_class()這個能體現雙親委派的函式中,如果類載入器物件不為空,則會呼叫這個類載入器的loadClass()函式(通過call_virtual()函式來呼叫)來載入類。
當然還會有其它方法,這裡就不一一列舉了。通過JavaCalls::call()、JavaCalls::call_helper()等函式呼叫Java方法,這些函式定義在JavaCalls類中,這個類的定義如下:
原始碼位置:/src/share/vm/runtime/javaCalls.hpp
// All calls to Java have to go via JavaCalls. Sets up the stack frame
// and makes sure that the last_Java_frame pointers are chained correctly. class JavaCalls: AllStatic {
static void call_helper(JavaValue* result, methodHandle* method, JavaCallArguments* args, TRAPS);
public:
// Optimized Constuctor call
static void call_default_constructor(JavaThread* thread, methodHandle method, Handle receiver, TRAPS); // call_special
// ------------
// The receiver must be first oop in argument list
// receiver表示方法的接收者,如A.main()呼叫中,A就是方法的接收者
static void call_special(JavaValue* result, KlassHandle klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS); static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS); // No args
static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS); // virtual call
// ------------ // The receiver must be first oop in argument list
static void call_virtual(JavaValue* result, KlassHandle spec_klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS); static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, TRAPS); // No args
static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS); // Static call
// -----------
static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, JavaCallArguments* args, TRAPS); static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS);
static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS); // Low-level interface
static void call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS);
};
可以看出,JavaCalls::call()函式為虛擬機器呼叫Java方法提供了便利。如上的函式都是自解釋的,對應各自的invoke*指令,Java虛擬機器有invokestatic、invokedynamic、invokestatic、invokespecial、invokevirtual幾種方法呼叫指令。這些call_static()、call_virtual()函式內部呼叫了call()函式。這一節我們先不介紹各個方法的具體實現。下一篇將詳細介紹。
我們選一個重要的main()方法來檢視具體的呼叫邏輯。如下基本照搬R大的內容,不過我略做了一些修改,如下:
假設我們的Java主類的類名為JavaMainClass,下面為了區分java launcher裡C/C++的main()與Java層程式裡的main(),把後者寫作JavaMainClass.main()方法。
從剛進入C/C++的main()函式開始:
啟動並呼叫HotSpot虛擬機器的main()函式的執行緒:
main()函式
-> //... 做一些引數檢查
-> //... 開啟新執行緒作為main執行緒,讓它從JavaMain()開始執行;該執行緒等待main執行緒執行結束
在如上執行緒中會啟動另外一個執行緒執行JavaMain()函式,如下:
JavaMain()
-> //... 找到指定的JVM
-> //... 載入並初始化JVM
-> //... 根據Main-Class指定的類名載入JavaMainClass
-> //... 在JavaMainClass類裡找到名為"main"的方法,簽名為"([Ljava/lang/String;)V",修飾符是public的靜態方法
-> (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); // 通過JNI呼叫JavaMainClass.main()方法
以上步驟都還在java launcher的控制下;當控制權轉移到JavaMainClass.main()方法之後就沒java launcher什麼事了,等JavaMainClass.main()方法返回之後java launcher才接手過來清理和關閉JVM。
下面看一下呼叫Java主類main()函式時會經過的主要方法及執行的主要邏輯,如下:
// HotSpot VM裡對JNI的CallStaticVoidMethod的實現。留意要傳給Java方法的引數以C的可變長度引數(…)傳入,這個函式將其收集打包為JNI_ArgumentPusherVaArg物件
-> jni_CallStaticVoidMethod()
// 這裡進一步將要傳給Java的引數轉換為JavaCallArguments物件傳下去
-> jni_invoke_static()
// 真正底層實現的開始。這個方法只是層皮,把JavaCalls::call_helper()用os::os_exception_wrapper()包裝起來,目的是設定HotSpot VM的C++層面的異常處理
-> JavaCalls::call()
-> JavaCalls::call_helper()
-> //... 檢查目標方法是否為空方法,是的話直接返回
-> //... 檢查目標方法是否“首次執行前就必須被編譯”,是的話呼叫JIT編譯器去編譯目標方法
-> //... 獲取目標方法的解釋模式入口from_interpreted_entry,下面將其稱為entry_point
-> //... 確保Java棧溢位檢查機制正確啟動
-> //... 建立一個JavaCallWrapper,用於管理JNIHandleBlock的分配與釋放,以及在呼叫Java方法前後儲存和恢復Java的frame pointer/stack pointer
-> StubRoutines::call_stub()( ... ) //... StubRoutines::call_stub()返回一個指向call stub的函式指標,緊接著呼叫這個call stub,傳入前面獲取的entry_point和要傳給Java方法的引數等資訊
// call stub是在VM初始化時生成的。對應的程式碼在StubGenerator::generate_call_stub()。它的功能可以參考程式碼前面的註釋。
-> //... 把相關暫存器的狀態調整到直譯器所需的狀態
-> //... 把要傳給Java方法的引數從JavaCallArguments物件解包展開到解釋模式calling convention所要求的位置
-> //... 跳轉到前面傳入的entry_point,也就是目標方法的from_interpreted_entry
-> //... 在-Xcomp模式下,實際跳入的是i2c adapter stub,將解釋模式calling convention傳入的引數挪到編譯模式calling convention所要求的位置
-> //... 跳轉到目標方法被JIT編譯後的程式碼裡,也就是跳到 nmethod 的 VEP 所指向的位置
-> //... 正式開始執行目標方法被JIT編譯好的程式碼 <- 這裡就是"main()方法的真正入口"
後面3個步驟是在編譯執行的模式下,不過後續我們從解釋執行開始研究,所以需要為虛擬機器配置-Xint選項,有了這個選項後,Java主類的main()方法就會解釋執行了。
在呼叫Java主類main()方法的過程中,我們看到了虛擬機器是通過JavaCalls::call()函式來間接呼叫main()方法的,下一篇我們研究一下具體的呼叫邏輯。
搭建過程中如果有問題可直接評論留言或加作者微信mazhimazh。
關注公眾號,有HotSpot原始碼剖析系列文章!