HotSpot的啟動過程(配視訊進行原始碼分析)
本文將詳細介紹HotSpot的啟動過程,啟動過程涉及到的邏輯比較複雜,細節也比較多,為了讓大家更快的瞭解這部分知識,我錄製了對應的視訊放到了B站上,大家可以參考。
第4節-HotSpot的啟動過程
下面我們開始以文章的形式簡單介紹一下啟動過程。
HotSpot通常會通過java.exe或javaw.exe來呼叫/jdk/src/share/bin/main.c檔案中的main()函式來啟動虛擬機器,使用Eclipse進行除錯時,也會呼叫到這個入口。main.c的main()函式負責建立執行環境,以及啟動一個全新的執行緒去執行JVM的初始化和呼叫Java程式的main()方法。main()函式最終會阻塞當前執行緒,同時用另外一個執行緒去呼叫JavaMain()函式。main()函式的呼叫棧如下:
main() main.c JLI_Launch() java.c JVMInit() java_md_solinux.c ContinueInNewThread() java.c ContinueInNewThread0() java_md_solinux.c pthread_join() pthread_join.c
呼叫鏈的順序從上到下,下面簡單介紹一下涉及到的相關方法。
1、main()函式
首先就是main()方法,方法的實現如下:
原始碼位置:/openjdk/jdk/src/share/bin/main.c #ifdef JAVAW char **__initenv; int WINAPI WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow){ int margc; char** margv; const jboolean const_javaw = JNI_TRUE; __initenv = _environ; #else /* JAVAW */ int main(int argc, char **argv){ int margc; char** margv; const jboolean const_javaw = JNI_FALSE; #endif /* JAVAW */ #ifdef _WIN32 { int i = 0; if (getenv(JLDEBUG_ENV_ENTRY) != NULL) { printf("Windows original main args:\n"); for (i = 0 ; i < __argc ; i++) { printf("wwwd_args[%d] = %s\n", i, __argv[i]); } } } JLI_CmdToArgs(GetCommandLine()); margc = JLI_GetStdArgc(); // add one more to mark the end margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *))); { int i = 0; StdArg *stdargs = JLI_GetStdArgs(); for (i = 0 ; i < margc ; i++) { margv[i] = stdargs[i].arg; } margv[i] = NULL; } #else /* *NIXES */ margc = argc; margv = argv; #endif /* WIN32 */ return JLI_Launch(margc, margv, sizeof(const_jargs) / sizeof(char *), const_jargs, sizeof(const_appclasspath) / sizeof(char *), const_appclasspath, FULL_VERSION, DOT_VERSION, (const_progname != NULL) ? const_progname : *margv, (const_launcher != NULL) ? const_launcher : *margv, (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE, const_cpwildcard, const_javaw, const_ergo_class); }
這個方法是Windows、UNIX、Linux以及Mac OS作業系統中C/C++的入口函式,而Windows的入口函式和其它的不太一樣,所以為了儘可能重用程式碼,這裡使用#ifdef條件編譯,所以對於基於Linux核心的Ubuntu來說,最終編譯的程式碼其實是如下的樣子:
int main(int argc, char **argv){ int margc; char** margv; const jboolean const_javaw = JNI_FALSE; margc = argc; margv = argv; return JLI_Launch(margc, margv, sizeof(const_jargs) / sizeof(char *), const_jargs, sizeof(const_appclasspath) / sizeof(char *), const_appclasspath, FULL_VERSION, DOT_VERSION, (const_progname != NULL) ? const_progname : *margv, (const_launcher != NULL) ? const_launcher : *margv, (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE, const_cpwildcard, const_javaw, const_ergo_class); }
第一個引數,int型的argc,為整型,用來統計程式執行時傳送給main函式的命令列引數的個數;第二個引數,char型的argv[],為字串陣列,用來存放指向的字串引數的指標陣列,每一個元素指向一個引數。
2、JLI_Launch()函式
JLI_Launch()函式進行了一系列必要的操作,如libjvm.so的載入、引數解析、Classpath的獲取和設定、系統屬性的設定、JVM 初始化等。函式會呼叫LoadJavaVM()載入libjvm.so並初始化相關引數,呼叫語句如下:
LoadJavaVM(jvmpath, &ifn)
其中jvmpath就是"/home/mazhi/workspace/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so",也就是libjvm.so的儲存路徑,而ifn是InvocationFunctions型別變數,InvocationFunctions的定義如下:
原始碼位置:/home/mazhi/workspace/openjdk/jdk/src/share/bin/java.h typedef jint (JNICALL *CreateJavaVM_t)(JavaVM **pvm, void **env, void *args); typedef jint (JNICALL *GetDefaultJavaVMInitArgs_t)(void *args); typedef jint (JNICALL *GetCreatedJavaVMs_t)(JavaVM **vmBuf, jsize bufLen, jsize *nVMs); typedef struct { CreateJavaVM_t CreateJavaVM; GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs; GetCreatedJavaVMs_t GetCreatedJavaVMs; } InvocationFunctions;
可以看到結構體InvocationFunctions中定義了3個函式指標,3個函式的實現在libjvm.so這個動態連結庫中,檢視LoadJavaVM()函式後就可以看到有如下實現:
ifn->CreateJavaVM = (CreateJavaVM_t) dlsym(libjvm, "JNI_CreateJavaVM"); ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs"); ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t) dlsym(libjvm, "JNI_GetCreatedJavaVMs");
所以通過函式指標呼叫時,最終會呼叫到libjvm.so中對應的以JNI_Xxx開頭的方法,其中JNI_CreateJavaVM()方法會在InitializeJVM()函式中呼叫,用來初始化2個JNI呼叫時非常重要的2個引數JavaVM和JNIEnv,後面在介紹JNI時還會詳細介紹,這裡不做過多介紹。
3、JVMInit()函式
JVMInit()函式的原始碼如下:
原始碼位置:/openjdk/jdk/src/solaris/bin/java_md_solinux.c int JVMInit(InvocationFunctions* ifn, jlong threadStackSize, int argc, char **argv, int mode, char *what, int ret){ // ... return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret); }
這個方法中沒有特別的邏輯,直接呼叫continueInNewThread()函式繼續執行相關邏輯。
4、ContinueInNewThread()函式
在JVMInit()函式中呼叫的ContinueInNewThread()函式的實現如下:
原始碼位置:/openjdk/jdk/src/share/bin/java.c int ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize, int argc, char **argv, int mode, char *what, int ret){ ... { /* Create a new thread to create JVM and invoke main method */ JavaMainArgs args; int rslt; args.argc = argc; args.argv = argv; args.mode = mode; args.what = what; args.ifn = *ifn; rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args); /* If the caller has deemed there is an error we * simply return that, otherwise we return the value of * the callee */ return (ret != 0) ? ret : rslt; } }
在呼叫ContinueInNewThread0()函式時,傳遞了JavaMain函式指標和呼叫此函式需要的引數args。
5、ContinueInNewthread0()函式
ContinueInNewThread()函式呼叫的ContinueInNewThread0()函式的實現如下:
位置:/openjdk/jdk/src/solaris/bin/java_md_solinux.c int ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) { int rslt; ... pthread_t tid; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); if (stack_size > 0) { pthread_attr_setstacksize(&attr, stack_size); } if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) { void * tmp; pthread_join(tid, &tmp); // 當前執行緒會阻塞在這裡 rslt = (int)tmp; } pthread_attr_destroy(&attr); ... return rslt; }
Linux 系統下(後面所說的Linux系統都是指基於Linux核心的作業系統)建立一個 pthread_t 執行緒,然後使用這個新建立的執行緒執行JavaMain()函式。
方法的第一個引數int (JNICALL continuation)(void )接收的就是JavaMain()函式的指標。關於指標函式與函式指標、以及Linux下建立執行緒的相關知識點後面會介紹,到時候這裡會給出連結。
下面就來看一下JavaMain()函式的實現,如下:
位置:/openjdk/jdk/src/share/bin/java.c int JNICALL JavaMain(void * _args){ JavaMainArgs *args = (JavaMainArgs *)_args; int argc = args->argc; char **argv = args->argv; InvocationFunctions ifn = args->ifn; JavaVM *vm = 0; JNIEnv *env = 0; jclass mainClass = NULL; jclass appClass = NULL; // actual application class being launched jmethodID mainID; jobjectArray mainArgs; // InitializeJVM 初始化JVM,給JavaVM和JNIEnv物件正確賦值,通過呼叫InvocationFunctions結構體下 // 的CreateJavaVM()函式指標來實現,該指標在LoadJavaVM()函式中指向libjvm.so動態連結庫中JNI_CreateJavaVM()函式 if (!InitializeJVM(&vm, &env, &ifn)) { JLI_ReportErrorMessage(JVM_ERROR1); exit(1); } // ... mainClass = LoadMainClass(env, mode, what); appClass = GetApplicationClass(env); mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); /* Build platform specific argument array */ mainArgs = CreateApplicationArgs(env, argv, argc); /* Invoke main method. */ (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); // ... }
程式碼主要就是找出Java原始碼的main()方法,然後呼叫並執行。
- 呼叫InitializeJVM()函式初始化JVM,主要就是初始化2個非常重要的變數JavaVM與JNIEnv,在這裡不過多探討這個問題,後面在講解JNI呼叫時會詳細介紹初始化過程。
- 呼叫LoadMainClass()函式獲取Java程式的啟動類,對於前面舉過的例項來說,由於配置了引數 “com.test/Test", 所以會查詢com.test.Test類。LoadMainClass()函式最終會呼叫libjvm.so中實現的JVM_FindClassFromBootLoader()方法來查詢啟動類,涉及到的邏輯比較多,後面在講解型別的載入時會介紹。
- 呼叫GetStaticMethodId()函式查詢Java啟動方法,其實就是獲取Test類中的main()方法。
- 呼叫JNIEnv中定義的CallStaticVoidMethod()方法,最終會呼叫JavaCalls::call()函式執行Test類中的main()方法。JavaCalls:call()函式是個非常重要的方法,後面在講解方法執行引擎時會詳細介紹。
以上步驟都還在當前執行緒的控制下。當控制權轉移到Test.main()之後當前執行緒就不再做其它事兒了,等Test.main()函式返回之後,當前執行緒會清理和關閉JVM。呼叫本地函式jni_DetachCurrentThread()斷開與主執行緒的連線。當成功與主執行緒斷開連線後,當前執行緒一直等待程式中所有的非守護執行緒全部執行結束,然後呼叫本地函式jni_DestroyJavaVM()對JVM執行銷燬。
其它文章:
1、在Ubuntu 16.04上編譯OpenJDK8的原始碼(配視訊)
2、除錯HotSpot原始碼(配視訊)
搭建過程中如果有問題可直接評論留言或加作者微信mazhimazh。
作者持續維護的個人部落格 classloading.com。
關注公眾號,有HotSpot原始碼剖析系列文章!
&n