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()方法,然后調用並執行。

  1. 調用InitializeJVM()函數初始化JVM,主要就是初始化2個非常重要的變量JavaVM與JNIEnv,在這里不過多探討這個問題,后面在講解JNI調用時會詳細介紹初始化過程。
  2. 調用LoadMainClass()函數獲取Java程序的啟動類,對於前面舉過的實例來說,由於配置了參數 “com.test/Test", 所以會查找com.test.Test類。LoadMainClass()函數最終會調用libjvm.so中實現的JVM_FindClassFromBootLoader()方法來查找啟動類,涉及到的邏輯比較多,后面在講解類型的加載時會介紹。
  3. 調用GetStaticMethodId()函數查找Java啟動方法,其實就是獲取Test類中的main()方法。
  4. 調用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源碼剖析系列文章!

   

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM