HotSpot學習(二):虛擬機的啓動過程源碼解析

1. 前言

上文介紹了HotSpot編譯和調試的方法,而這篇文章將邁出正式調試的第一步——調試HotSpot的啓動過程。
學習啓動過程能夠幫助咱們瞭解程序的入口,並對虛擬機的運行有個總體的把握,方便往後深刻學習具體的一些模塊。java

2. 總體感知啓動過程

總體的感知啓動過程能夠在啓動時添加_JAVA_LAUNCHER_DEBUG=1的環境變量。這樣JVM會輸出詳細的打印。
經過這些打印,咱們大體能瞭解到啓動過程發生了什麼。linux

----_JAVA_LAUNCHER_DEBUG----
Launcher state:
	debug:on
	javargs:off
	program name:java
	launcher name:openjdk
	javaw:off
	fullversion:1.8.0-internal-debug-xieshang_2020_12_18_09_49-b00
	dotversion:1.8
	ergo_policy:DEFAULT_ERGONOMICS_POLICY
Command line args:
argv[0] = /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java
argv[1] = com.insanexs/HelloHotspot
JRE path is /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk
jvm.cfg[0] = ->-server<-
jvm.cfg[1] = ->-client<-
1 micro seconds to parse jvm.cfg
Default VM: server
Does `/home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so' exist ... yes.
mustsetenv: FALSE
JVM path is /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so
1 micro seconds to LoadJavaVM
JavaVM args:
    version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 5
    option[ 0] = '-Dsun.java.launcher.diag=true'
    option[ 1] = '-Djava.class.path=/home/xieshang/learn-open-jdk'
    option[ 2] = '-Dsun.java.command=com.insanexs/HelloHotspot'
    option[ 3] = '-Dsun.java.launcher=SUN_STANDARD'
    option[ 4] = '-Dsun.java.launcher.pid=4485'
1 micro seconds to InitializeJVM
Main class is 'com.insanexs/HelloHotspot'
App's argc is 0
1 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----

從上面的打印大體能夠看出有這麼幾步:windows

  1. 打印了啓動器的狀態,包括版本號、程序名等
  2. 打印了傳給程序命令行參數,第一個是java命令的相信路徑,第二個虛擬機將要執行的java代碼
  3. 解析JRE路徑,解析jvm.cfg
  4. 加載libjvm庫
  5. 解析虛擬機參數
  6. 初始化虛擬機
  7. 虛擬機加載要執行的Java主類,解析參數並執行

3. 啓動過程說明

咱們就以上面劃分的階段爲總體脈絡,再深刻的看看各階段的具體邏輯。數據結構

3.1 啓動入口

虛擬機程序運行的入口是在main.c/main方法中。以後會調用java.c/JLI_Launch方法。app

int
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 */
        jint ergo                               /* ergonomics class policy */
)
{   
    /************************** 前期初始化工做和狀態打印 ********************/
    int mode = LM_UNKNOWN;
    char *what = NULL;
    char *cpath = 0;
    char *main_class = NULL;
    int ret;
    InvocationFunctions ifn; //和建立虛擬機相關的結構體 指向三個關鍵的函數
    jlong start, end;
    char jvmpath[MAXPATHLEN];
    char jrepath[MAXPATHLEN];
    char jvmcfg[MAXPATHLEN];

    _fVersion = fullversion;
    _dVersion = dotversion;
    _launcher_name = lname;
    _program_name = pname;
    _is_java_args = javaargs;
    _wc_enabled = cpwildcard;
    _ergo_policy = ergo;

    InitLauncher(javaw);
    DumpState(); //打印相關狀態
    
    //打印參數
    if (JLI_IsTraceLauncher()) {
        int i;
        printf("Command line args:\n");
        for (i = 0; i < argc ; i++) {
            printf("argv[%d] = %s\n", i, argv[i]);
        }
        AddOption("-Dsun.java.launcher.diag=true", NULL);
    }
    
    /************************** 檢驗版本 ********************/
    /*
     * Make sure the specified version of the JRE is running.
     *
     * There are three things to note about the SelectVersion() routine:
     *  1) If the version running isn't correct, this routine doesn't
     *     return (either the correct version has been exec'd or an error
     *     was issued).
     *  2) Argc and Argv in this scope are *not* altered by this routine.
     *     It is the responsibility of subsequent code to ignore the
     *     arguments handled by this routine.
     *  3) As a side-effect, the variable "main_class" is guaranteed to
     *     be set (if it should ever be set).  This isn't exactly the
     *     poster child for structured programming, but it is a small
     *     price to pay for not processing a jar file operand twice.
     *     (Note: This side effect has been disabled.  See comment on
     *     bugid 5030265 below.)
     */
    SelectVersion(argc, argv, &main_class); //版本檢測
    
    /************************** 建立執行環境 ********************/
    CreateExecutionEnvironment(&argc, &argv,
                               jrepath, sizeof(jrepath),
                               jvmpath, sizeof(jvmpath),
                               jvmcfg,  sizeof(jvmcfg));//解析相關環境 獲取jre路徑、jvmlib庫和jvm.cfg
    
    /************************** 設置虛擬機環境 ********************/
    if (!IsJavaArgs()) {
        SetJvmEnvironment(argc,argv);
    }

    ifn.CreateJavaVM = 0;
    ifn.GetDefaultJavaVMInitArgs = 0;

    if (JLI_IsTraceLauncher()) {
        start = CounterGet();
    }
    
    /************************** 加載虛擬機 ********************/
    if (!LoadJavaVM(jvmpath, &ifn)) { //加載 主要是從jvmlib庫中解析函數地址 賦值給ifn
        return(6);
    }

    if (JLI_IsTraceLauncher()) {
        end   = CounterGet();
    }

    JLI_TraceLauncher("%ld micro seconds to LoadJavaVM\n",
             (long)(jint)Counter2Micros(end-start));

    ++argv;
    --argc;

    if (IsJavaArgs()) {
        /* Preprocess wrapper arguments */
        TranslateApplicationArgs(jargc, jargv, &argc, &argv);
        if (!AddApplicationOptions(appclassc, appclassv)) {
            return(1);
        }
    } else {
        /* Set default CLASSPATH */
        cpath = getenv("CLASSPATH"); //添加CLASSPATH
        if (cpath == NULL) {
            cpath = ".";
        }
        SetClassPath(cpath);
    }
    
    /************************** 解析參數 ********************/
    /* Parse command line options; if the return value of
     * ParseArguments is false, the program should exit.
     */
    if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
    {
        return(ret);
    }

    /* Override class path if -jar flag was specified */
    if (mode == LM_JAR) { //若是是java -jar 則覆蓋classpath
        SetClassPath(what);     /* Override class path */
    }

    /* set the -Dsun.java.command pseudo property */ //解析特殊屬性
    SetJavaCommandLineProp(what, argc, argv);

    /* Set the -Dsun.java.launcher pseudo property */
    SetJavaLauncherProp();

    /* set the -Dsun.java.launcher.* platform properties */
    SetJavaLauncherPlatformProps();
    
    /************************** 初始化虛擬機 ********************/
    return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}

這個方法比較長,可是能夠劃分爲幾個部分去分析:jvm

3.1.1 前期初始化工做和狀態打印

這裏的初始化部分包括一些參數值的聲明,特殊結構體InvocationFuntions的聲明,啓動器的初始化。
其中聲明的參數會在後續的啓動過程用來存儲相關信息,例如保存JVM、JRE相關路徑等。
InvocationFuntions是個重要的結構體,其中包含了建立JVM會被調用的三個函數指針。ide

typedef struct {
    CreateJavaVM_t CreateJavaVM; //指向負責建立JavaVM和JNIEnv結構的函數指針
    GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs; //指向獲取默認JVM初始參數的函數指針
    GetCreatedJavaVMs_t GetCreatedJavaVMs; //指向獲取JVM的函數指針
} InvocationFunctions;

InitLaucher方法主要就是根據_JAVA_LAUNCHER_DEBUG這個環境變量會決定後續是否輸出DEBUG的打印。
在開啓了launcher_debug後,DumpState()方法會打印出啓動狀態,而且以後打印出命令行參數。函數

3.1.2 檢驗版本

SelectVersion會驗證用戶指定的java版本和實際執行的java版本是否兼容,若是不兼容會退出進程。用戶能夠經過_JAVA_VERSION_SET的環境變量或是jar包中manifest文件等方式指定運行的java版本。工具

3.1.3 建立執行環境

CreateExecutionEnvironment會爲後續的啓動建立執行環境,這一步驟中主要是肯定jdk所在的路徑,解析jvmcfg和確認libjvm是否存在等。post

  1. 主要是根據處理器類型和主路徑肯定出JRE的路徑
  2. 以一樣的方式肯定jvm.cfg的文件位置,並解析jvm.cfg(jvm.cfg裏面是一些虛擬機的默認配置,如常見的指定以客戶端或服務端模式運行)
  3. 檢查虛擬機類型(-server/-client),能夠是jvm.cfg指定或是由啓動參數指定
  4. 肯定libjvm庫的位置,校驗庫是否存在,這個庫核心的函數庫
3.1.4 設置虛擬機環境

SetJvmEnviroment主要解析NativeMemoryTracking參數,能夠用來追蹤本地內存的使用狀況

3.1.5 加載虛擬機

前期環境準備好以後,LoadJavaVM()會從以前肯定的路徑,加載libjvm庫,並將其中的庫中JNI_CreateJavaVM,JNI_GetDefaultJavaVMInitArgsJNI_GetCreatedJavaVMs三個函數賦值給ifn。
這三個函數會在以後建立虛擬機時被使用。

3.1.6 解析參數

這裏有兩個部分,一是解析命令行傳入的參數,看是否有特定的JVM配置選項。這些參數會被用於後續虛擬機的建立上。這一過程主要發生在ParseArguments()中。另外一個部分就是添加一些特定的虛擬機參數,發生在SetJavaCommandLinePropSetJavaLaucherPropSetJavaLaucherPlatformProps中。

3.1.7 虛擬機初始化

在環境都準備好以後,會由JVMInit()執行虛擬機初始化工做,首先會經過ShowSplashScreen()方法加載啓動動畫,以後會進入CountinueInNewThread()方法,由新的線程負責建立虛擬機的工做。

3.2 在新線程中繼續虛擬機的建立

經過上文的介紹,咱們找到了java.c/ConutinueInNewThread()的方法。這個方法分爲兩個部分,第一部分就是肯定線程棧的深度,第二部分就是由ContinueInNewThread0()這個方法實現真正的虛擬機建立過程。

int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
    int rslt;
#ifndef __solaris__
    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;
    } else {
     /*
      * Continue execution in current thread if for some reason (e.g. out of
      * memory/LWP)  a new thread can't be created. This will likely fail
      * later in continuation as JNI_CreateJavaVM needs to create quite a
      * few new threads, anyway, just give it a try..
      */
      rslt = continuation(args);
    }

    pthread_attr_destroy(&attr);
#else /* __solaris__ */
    thread_t tid;
    long flags = 0;
    if (thr_create(NULL, stack_size, (void *(*)(void *))continuation, args, flags, &tid) == 0) {
      void * tmp;
      thr_join(tid, NULL, &tmp);
      rslt = (int)tmp;
    } else {
      /* See above. Continue in current thread if thr_create() failed */
      rslt = continuation(args);
    }
#endif /* !__solaris__ */
    return rslt;
}

在這個方法中,首先調用了pthread_create()函數建立了一個新線程,同時舊線程被jion等待新線程運行完成後返回。
pthread_create()是unix操做系統建立線程的函數,它的第一個參數表示線程標識,第二參數表示線程屬性,第三個參數表示建立線程所要執行函數的地址,第四個參數則是將要執行的函數的參數。
等到新線程運行完成後,舊的線程也會返回。此時說明運行結束,進程將會退出。

須要注意的是此時傳入的函數地址,它是指向java.c/JavaMain()函數。也就是說新建立的線程將會開始執行該函數。

3.3 虛擬機建立、Java程序運行的主過程——JavaMain

新建立的線程會去執行JavaMain()函數,正式進入了建立虛擬機、運行Java代碼的過程。

int JNICALL
JavaMain(void * _args)
{   
    /*********************獲取相關參數****************************/
    JavaMainArgs *args = (JavaMainArgs *)_args;
    int argc = args->argc;
    char **argv = args->argv;
    int mode = args->mode;
    char *what = args->what;
    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;
    int ret = 0;
    jlong start, end;

    RegisterThread();

    /*******************初始化JVM、打印相關信息********************************/
    start = CounterGet();
    if (!InitializeJVM(&vm, &env, &ifn)) {
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }

    if (showSettings != NULL) {
        ShowSettings(env, showSettings);
        CHECK_EXCEPTION_LEAVE(1);
    }

    if (printVersion || showVersion) {
        PrintJavaVersion(env, showVersion);
        CHECK_EXCEPTION_LEAVE(0);
        if (printVersion) {
            LEAVE();
        }
    }

    /* If the user specified neither a class name nor a JAR file */
    if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
        PrintUsage(env, printXUsage);
        CHECK_EXCEPTION_LEAVE(1);
        LEAVE();
    }

    FreeKnownVMs();  /* after last possible PrintUsage() */

    if (JLI_IsTraceLauncher()) {
        end = CounterGet();
        JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n",
               (long)(jint)Counter2Micros(end-start));
    }

    /* At this stage, argc/argv have the application's arguments */
    //打印Java程序的參數
    if (JLI_IsTraceLauncher()){
        int i;
        printf("%s is '%s'\n", launchModeNames[mode], what);
        printf("App's argc is %d\n", argc);
        for (i=0; i < argc; i++) {
            printf("    argv[%2d] = '%s'\n", i, argv[i]);
        }
    }
    
    /******************獲取Java程序的主類***************************/
    ret = 1;

    /*
     * Get the application's main class.
     *
     * See bugid 5030265.  The Main-Class name has already been parsed
     * from the manifest, but not parsed properly for UTF-8 support.
     * Hence the code here ignores the value previously extracted and
     * uses the pre-existing code to reextract the value.  This is
     * possibly an end of release cycle expedient.  However, it has
     * also been discovered that passing some character sets through
     * the environment has "strange" behavior on some variants of
     * Windows.  Hence, maybe the manifest parsing code local to the
     * launcher should never be enhanced.
     *
     * Hence, future work should either:
     *     1)   Correct the local parsing code and verify that the
     *          Main-Class attribute gets properly passed through
     *          all environments,
     *     2)   Remove the vestages of maintaining main_class through
     *          the environment (and remove these comments).
     *
     * This method also correctly handles launching existing JavaFX
     * applications that may or may not have a Main-Class manifest entry.
     */
    mainClass = LoadMainClass(env, mode, what);//加載mainClass
    CHECK_EXCEPTION_NULL_LEAVE(mainClass);
    /*
     * In some cases when launching an application that needs a helper, e.g., a
     * JavaFX application with no main method, the mainClass will not be the
     * applications own main class but rather a helper class. To keep things
     * consistent in the UI we need to track and report the application main class.
     */
    appClass = GetApplicationClass(env); //獲取application class
    NULL_CHECK_RETURN_VALUE(appClass, -1);
    /*
     * PostJVMInit uses the class name as the application name for GUI purposes,
     * for example, on OSX this sets the application name in the menu bar for
     * both SWT and JavaFX. So we'll pass the actual application class here
     * instead of mainClass as that may be a launcher or helper class instead
     * of the application class.
     */
    PostJVMInit(env, appClass, vm); // JVM 初始化後置處理
    /*
     * The LoadMainClass not only loads the main class, it will also ensure
     * that the main method's signature is correct, therefore further checking
     * is not required. The main method is invoked here so that extraneous java
     * stacks are not in the application stack trace.
     */
     
    /******************找主類的main方法************************/
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V"); //獲取main class的 main(String[] args)方法
    CHECK_EXCEPTION_NULL_LEAVE(mainID);
    
    /*******************封裝參數,調用main方法*****************/
    /* Build platform specific argument array */
    mainArgs = CreateApplicationArgs(env, argv, argc); //封裝 main(String[] args) 方法的參數args
    CHECK_EXCEPTION_NULL_LEAVE(mainArgs);

    /* Invoke main method. */
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); //調用main(String args)方法

    /*
     * The launcher's exit code (in the absence of calls to
     * System.exit) will be non-zero if main threw an exception.
     */
    /*******************獲取執行結果 並退出虛擬機**************/
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1; //根據是否有異常 肯定退出碼
    LEAVE(); //線程解綁 銷燬JVM
}
3.3.1 參數解析

以前上文解析獲得的命令行參數等都被封裝在JavaMainArgs結構體中,傳給了JavaMain方法。所以須要從這個結構體中取回參數。
另外,還建立了一些變量用於以後的過程當中存儲值,譬如jclass,jmethodID等。

3.3.2 初始化虛擬機,打印相關信息

上述代碼中的InitializeJVM()方法會負責虛擬機的初始化過程。其代碼以下:

static jboolean
InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
{
    JavaVMInitArgs args;
    jint r;

    memset(&args, 0, sizeof(args));
    args.version  = JNI_VERSION_1_2;
    args.nOptions = numOptions;
    args.options  = options;
    args.ignoreUnrecognized = JNI_FALSE;

    if (JLI_IsTraceLauncher()) {
        int i = 0;
        printf("JavaVM args:\n    ");
        printf("version 0x%08lx, ", (long)args.version);
        printf("ignoreUnrecognized is %s, ",
               args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE");
        printf("nOptions is %ld\n", (long)args.nOptions);
        for (i = 0; i < numOptions; i++)
            printf("    option[%2d] = '%s'\n",
                   i, args.options[i].optionString);
    }

    r = ifn->CreateJavaVM(pvm, (void **)penv, &args); //經過ifn的函數指針 調用CreateJavaVM函數初始化JavaVM 和 JNIEnv
    JLI_MemFree(options);
    return r == JNI_OK;
}

先獲取虛擬機參數,在經過ifn結構體中CreateJavaVM指針,調用正式建立Java虛擬機的函數JNI_CreateJavaVM
JNI_CreateJavaVM代碼的主要流程以下:

  1. 先由Threads::create_vm()方法建立虛擬機
  2. 給兩個重要的指針賦值,分別是JavaVM * 和 JNIEnv
  3. 一些後置處理,例如經過JVMTI(能夠說是虛擬機的工具接口,提供了對虛擬機調試、監測等等的功能)、事件提交等

針對第一點,Threads::create_vm()是負責建立虛擬機,整個過程相對複雜,須要初始化不少模塊,建立虛擬機的後臺線程,加載必要的類等等,這裏不作深刻分析。以後有時間能夠單獨分析這一過程。
針對第二點中提到的兩個數據結構,很是重要。咱們能夠看看它們的具體的內容。

JavaVM

JavaVM結構內部包的是JNIInvokeInterface_結構,所以咱們直接看一下JNIInvokeInterface_的結構

struct JNIInvokeInterface_ {
    //預留字段
    void *reserved0;
    void *reserved1;
    void *reserved2;

    jint (JNICALL *DestroyJavaVM)(JavaVM *vm); //銷燬虛擬機的函數指針

    jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args); //綁定線程的函數指針

    jint (JNICALL *DetachCurrentThread)(JavaVM *vm); //解綁線程的函數指針

    jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version); //獲取JNIEnv結構的函數指針

    jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);//將線程轉爲後臺線程
};

能夠看到主要是一些和虛擬機操做的相關函數。

JNIEnv

JNIEnv結構內部包的是JNINativeInterface結構,這個結構一樣定義了不少函數指針,代碼太長,這裏就不直接貼出了。有興趣的能夠在jni.h中自行查看。若是對結構中的方法分類的話,能夠分紅如下幾類:

  • 獲取虛擬機信息
  • 獲取相關類和方法,方法執行
  • 獲取/設置對象字段
  • 靜態方法、靜態變量的獲取與設置
  • 常見類型的對象的建立和釋放
  • 建立直接內存、訪問鎖等

總之,提供了經過C++代碼訪問Java程序的能力(這對於從事JNI開發的人來講十分重要)。

3.3.3 肯定Java程序的主類

瞭解完成虛擬機的初始化過程後,再回到JavaMain()方法中,以後是經過LoadMainClass()GetApplicationClass()方法肯定Java代碼的主類。
若是咱們在運行指定了Java類,那麼這個類就是主類。這裏還會調用LauncherHelper.checkAndLoadMain()檢驗主類是否合法。LauncherHelper的Java代碼,這裏就是上面介紹的JNIEnv的能力在C++的代碼中執行Java代碼。
對於一些沒有主類的程序,須要經過LaucherHelper.getApplicationClass()肯定程序類。

3.3.4 從主類中獲取main方法的methodID,並調用方法

再肯定了mainClass以後,還須要找到該類定義的main(),獲取main()方法,而後將程序參數封裝,傳遞給main()執行,線程會以此爲入口,開始執行Java程序。
這裏的找方法和執行方法一樣是依賴了JNIEnv中GetStaticMethodIDCallStaticVoidMethod
因此咱們的main()方法老是static void的。

3.3.5 獲取執行結果,退出虛擬機

當線程從Main()方法中返回,說明Java程序已經執行完成(或是異常退出),這時候虛擬機會檢查運行結果,並解綁線程銷燬虛擬機,最終退出。

相關文章
相關標籤/搜索