[Inside hotspot]hotspot的啟動流程與main方法呼叫
hotspot的啟動流程與main方法呼叫
虛擬機器的使命就是執行public static void main(String[])
方法,從虛擬機器建立到main方法執行會經過一系列流程。這篇文章詳細討論了執行命令列java.exe HelloWorld
呼叫main函式輸出經歷了什麼。原始碼使用openjdk12
,作業系統為windows 64bits
,其它系統和原始碼版本大同小異。
java.base
首先要明白一個概念,java.exe
大體上可以分為啟動器部分和hotspot部分。
啟動器負責執行一些命令列解析,環境初始化等任務,hotspot部分則是真正的虛擬機器幹活的地方。
啟動器是用C++寫的,如果不修改連結器入口點名字,執行java.exe xxx
追根溯源必然會跟蹤到main
函式。這個main位於
openjdk12\src\java.base\share\native\launcher\main.c
,這裡就是java啟動器的最終起源了:
#ifdef JAVAW char **__initenv; int WINAPI WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow) { int margc; char** margv; int jargc; char** jargv; const jboolean const_javaw = JNI_TRUE; __initenv = _environ; #else /* JAVAW */ JNIEXPORT int main(int argc, char **argv) { int margc; char** margv; int jargc; char** jargv; const jboolean const_javaw = JNI_FALSE; #endif /* JAVAW */ // 處理傳遞給啟動器的引數 return JLI_Launch(margc, margv, jargc, (const char**) jargv, 0, NULL, VERSION_STRING, DOT_VERSION, (const_progname != NULL) ? const_progname : *margv, (const_launcher != NULL) ? const_launcher : *margv, jargc > 0, const_cpwildcard, const_javaw, 0); }
如果使用者執行的是javaw.exe
就進入WinMain
入口,否則java.exe
進入main
入口。
在main中會處理啟動器的引數比如這種-XX:+UnlockDiagnosticVMOptions -XX:+PauseAtExit
,處理完之後呼叫JLI_Launcher
。多說一點,啟動器程式碼也分為系統相關和系統無關,像java.base/linux
,java.base/windows
這種就是平臺相關,java.base/share
就是平臺無關程式碼。
java.base -> JLI_Launcher
JLI_Launcher位於openjdk12\src\java.base\share\native\libjli\java.c
,
JNIEXPORT int JNICALL JLI_Launch(int argc, char ** argv,/* main argc, argc */ int jargc, const char** jargv,/* java args */ int appclassc, const char** appclassv,/* app classpath */ const char* fullversion,/* full version defined */ const char* dotversion,/* dot version defined */ const char* pname,/* program name */ const char* lname,/* launcher name */ jboolean javaargs,/* JAVA_ARGS */ jboolean cpwildcard,/* classpath wildcard */ jboolean javaw,/* windows-only javaw */ jintergo_class/* ergnomics policy */ );
JLI_Launcher做了很多重要的事情
-XX:SDKGJ
第三點非常重要,它是啟動器呼叫hotspot JNI的橋樑。說著這麼誇張,其實做起來是非常簡單的,就是LoadLibrary()
載入jvm.dll然後GetProcAddress()
執行時獲取JNI_CreateJavaVM地址轉化為函式指標,對應linux的dlopen,dlsym
。然後經過一些中轉,啟動器會走到JavaMain。
jaba.base -> JLI_Launcher -> JavaMain
JavaMain維護hotspot的一個生命週期,它溝通java啟動器與hotspot世界,完成java.exe的功能:
int JNICALL JavaMain(void * _args) { ... /* 初始化虛擬機器 */ start = CounterGet(); /* 這裡初始化虛擬機器呼叫的就是之前提到的在jvm.dll裡面獲取到的JNI_CreateJavaVM函式指標 * 可以說,這裡的JNI_CreateJavaVM是hotspot世界最先出現的地方 */ if (!InitializeJVM(&vm, &env, &ifn)) { JLI_ReportErrorMessage(JVM_ERROR1); exit(1); } /* 對虛擬機器啟動進行效能profiling */ if (JLI_IsTraceLauncher()) { end = CounterGet(); JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n", (long)(jint)Counter2Micros(end-start)); } ret = 1; /* 載入main函式所在的類 */ mainClass = LoadMainClass(env, mode, what); CHECK_EXCEPTION_NULL_LEAVE(mainClass); /* 對GUI程式的支援 */ appClass = GetApplicationClass(env); mainArgs = CreateApplicationArgs(env, argv, argc); if (dryRun) { ret = 0; LEAVE(); } PostJVMInit(env, appClass, vm); /* 獲取main方法id */ mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); /* main方法呼叫 */ (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); /* 啟動器的返回值(非System.exit退出) */ ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1; LEAVE(); }
JavaMain這個函式做了我們通常意義上所認為啟動器應該做的事情,它:
- 初始化虛擬機器,
- 獲取main所在的類
- 呼叫main方法
- 處理返回值
到這裡java啟動器流程基本上已經清晰了,但是旅程並未結束。除了java啟動器外,本文還想探究一下main方法的呼叫。
jaba.base -> JLI_Launcher -> JavaMain -> CallStaticVoidMethod
首先,歡迎來到hotspot的世界。前面說到main方法的呼叫是這麼一行程式碼:
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
那麼它是怎麼進入hotspot的世界的呢,要回答這個問題得看看env這是個什麼東西。
env類似於這樣一個結構:
struct P{ void (*jni_f1)(int,int); void (*jni_f2)(); void (*jni_f3)(double); }; P* env;
然後(*env)->jni_f1(3,4)
呼叫的就是這個jni_f1函式指標,這些指標指向的是hotspot/share/primes/jni.cpp
裡面的入口點。
jaba.base -> JLI_Launcher -> JavaMain -> CallStaticVoidMethod -> JavaCalls::call
回到上面的程式碼,CallStaticVoidMethod函式指標指向的就是JNI裡面的函式:
JNI_ENTRY(void, jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...)) JNIWrapper("CallStaticVoidMethod"); HOTSPOT_JNI_CALLSTATICVOIDMETHOD_ENTRY(env, cls, (uintptr_t) methodID); DT_VOID_RETURN_MARK(CallStaticVoidMethod); va_list args; va_start(args, methodID); JavaValue jvalue(T_VOID); JNI_ArgumentPusherVaArg ap(methodID, args); jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK); va_end(args); JNI_END static void jni_invoke_static(JNIEnv *env, JavaValue* result, jobject receiver, JNICallType call_type, jmethodID method_id, JNI_ArgumentPusher *args, TRAPS) { methodHandle method(THREAD, Method::resolve_jmethod_id(method_id)); // 建立java呼叫的引數 ResourceMark rm(THREAD); int number_of_parameters = method->size_of_parameters(); JavaCallArguments java_args(number_of_parameters); args->set_java_argument_object(&java_args); assert(method->is_static(), "method should be static"); args->iterate( Fingerprinter(method).fingerprint() ); // 初始化返回值型別 result->set_type(args->get_ret_type()); // main方法呼叫 JavaCalls::call(result, method, &java_args, CHECK); // 返回值轉換 if (result->get_type() == T_OBJECT || result->get_type() == T_ARRAY) { result->set_jobject(JNIHandles::make_local(env, (oop) result->get_jobject())); } }
jni_CallStaticVoidMethod只是處理了一下可變引數,其他工作交給jni_invoke_static。這個函式會把之前傳入的命令列引數轉換為虛擬機器裡面的oop物件,然後最終通過JavaCalls::call
呼叫了main函式。這不是個特例,java的所有方法呼叫都是通過JavaCalls::call
呼叫的,它會建立解釋執行所需的棧幀,然後識別hotspot的模板直譯器入口點,進入這個入口點執行位元組碼,當然這都是後話,如果有的話...