美文网首页
Java Hotspot虚拟机的启动过程(二)

Java Hotspot虚拟机的启动过程(二)

作者: buzzerrookie | 来源:发表于2019-03-03 10:23 被阅读0次

    本文接着前一篇文章继续分析Hotspot虚拟机的启动过程,调用LoadJavaVM函数加载JVM动态库后还需要解析其他的命令行选项。

    解析其他命令行选项

    解析其他命令行选项的代码如下:

    ++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");
        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) {
        SetClassPath(what);     /* Override class path */
    }
    

    第一行++argv跳过了可执行文件名,开始处理其他命令行选项。
    前一篇文章提到IsJavaArgs()函数返回JAVA_ARGS宏是否被定义,因此如果JAVA_ARGS宏有定义,那么首先处理宏里面的选项。
    还是以javac命令为例,编译javac命令的部分Makefile如下:

    $(eval $(call SetupLauncher,javac, \
        -DEXPAND_CLASSPATH_WILDCARDS \
        -DNEVER_ACT_AS_SERVER_CLASS_MACHINE \
        -DJAVA_ARGS='{ "-J-ms8m"$(COMMA) "com.sun.tools.javac.Main"$(COMMA) }'))
    

    javac命令运行时main函数的argc、argv是正常的命令行参数,即javac的选项和参数,main函数调用JLI_Launch时,margc和margv分别是argc和argv,而const_jargs就是JAVA_ARGS宏{ "-J-ms8m", "com.sun.tools.javac.Main", }

    TranslateApplicationArgs函数

    TranslateApplicationArgs函数将命令行参数和JAVA_ARGS合并成新的命令行参数,代码如下:

    static void
    TranslateApplicationArgs(int jargc, const char **jargv, int *pargc, char ***pargv)
    {
        int argc = *pargc;
        char **argv = *pargv;
        int nargc = argc + jargc;
        char **nargv = JLI_MemAlloc((nargc + 1) * sizeof(char *));
        int i;
    
        *pargc = nargc;
        *pargv = nargv;
    
        /* Copy the VM arguments (i.e. prefixed with -J) */
        for (i = 0; i < jargc; i++) {
            const char *arg = jargv[i];
            if (arg[0] == '-' && arg[1] == 'J') {
                *nargv++ = ((arg + 2) == NULL) ? NULL : JLI_StringDup(arg + 2);
            }
        }
    
        for (i = 0; i < argc; i++) {
            char *arg = argv[i];
            if (arg[0] == '-' && arg[1] == 'J') {
                if (arg[2] == '\0') {
                    JLI_ReportErrorMessage(ARG_ERROR3);
                    exit(1);
                }
                *nargv++ = arg + 2;
            }
        }
    
        /* Copy the rest of the arguments */
        for (i = 0; i < jargc ; i++) {
            const char *arg = jargv[i];
            if (arg[0] != '-' || arg[1] != 'J') {
                *nargv++ = (arg == NULL) ? NULL : JLI_StringDup(arg);
            }
        }
        for (i = 0; i < argc; i++) {
            char *arg = argv[i];
            if (arg[0] == '-') {
                if (arg[1] == 'J')
                    continue;
                if (IsWildCardEnabled() && arg[1] == 'c'
                    && (JLI_StrCmp(arg, "-cp") == 0 ||
                        JLI_StrCmp(arg, "-classpath") == 0)
                    && i < argc - 1) {
                    *nargv++ = arg;
                    *nargv++ = (char *) JLI_WildcardExpandClasspath(argv[i+1]);
                    i++;
                    continue;
                }
            }
            *nargv++ = arg;
        }
        *nargv = 0;
    }
    

    构造一个新的命令行参数列表:

    • 首先处理JAVA_ARGS中以-J开头的选项,去掉-J并添加到新的参数列表中;
    • 然后处理原命令行参数中以-J开头的选项,去掉-J并添加到新的参数列表中;
    • 接着处理JAVA_ARGS中其他不以-J开头的选项,添加到新的参数列表中;
    • 最后添加原命令行参数中其他不以-J开头的选项,其中对-cp或-classpath后跟的路径做了特殊处理:如果路径含有通配符,那么该路径会被展开一层(非递归),用展开后的各文件/目录路径组成以分号分隔的新字符串替换原路径参数。

    注意参数pargc和pargv都是指针,所以该函数返回后JLI_Launch函数里的argc和argv分别是新的参数个数和列表了,并且第一个参数已经不再是可执行文件名。

    AddApplicationOptions函数

    AddApplicationOptions函数为JVM启动添加了应用选项:

    • 如果CLASSPATH环境变量被设置,如果没有通配符则添加新的虚拟机启动选项:-Denv.class.path=变量值;如果有通配符则将变量值进行通配符展开(与TranslateApplicationArgs函数中的相同),添加新的虚拟机启动选项:-Denv.class.path=展开后的各文件/目录路径组成的以分号分隔的字符串;
    • 添加新的虚拟机启动选项:-Dapplication.home=可执行文件的应用目录(见GetApplicationHome函数);
    • 添加新的虚拟机启动选项:-Djava.class.path=可执行文件的应用目录内的某些子目录/文件路径组成以分号分隔的字符串, 具体哪些子目录/文件的路径需要被添加由APP_CLASSPATH宏定义。

    SetClassPath函数

    上面分析完了if分支,这里分析else分支。如果不是Launcher(即JAVA_ARGS宏没有定义),那么检查环境变量CLASSPATH,如果没有设置则默认CLASSPATH是当前目录。
    如果环境变量CLASSPATH没有通配符那么SetClassPath函数添加新的虚拟机启动参数:-Denv.class.path=变量值;如果有通配符那么SetClassPath函数将变量值进行通配符展开(与TranslateApplicationArgs函数中的相同),添加新的虚拟机启动参数:-Denv.class.path=展开后的各文件/目录路径组成的以分号分隔的字符串。

    ParseArguments函数

    ParseArguments函数解析新的命令行参数列表,如果不符合要求则报错,否则调用AddOption函数添加到虚拟机的启动选项中。

    static jboolean
    ParseArguments(int *pargc, char ***pargv,
                   int *pmode, char **pwhat,
                   int *pret, const char *jrepath)
    {
        int argc = *pargc;
        char **argv = *pargv;
        int mode = LM_UNKNOWN;
        char *arg;
    
        *pret = 0;
    
        while ((arg = *argv) != 0 && *arg == '-') {
            argv++; --argc;
            if (JLI_StrCmp(arg, "-classpath") == 0 || JLI_StrCmp(arg, "-cp") == 0) {
                ARG_CHECK (argc, ARG_ERROR1, arg);
                SetClassPath(*argv);
                mode = LM_CLASS;
                argv++; --argc;
            } else if (JLI_StrCmp(arg, "-jar") == 0) {
                ARG_CHECK (argc, ARG_ERROR2, arg);
                mode = LM_JAR;
            } else if (JLI_StrCmp(arg, "-help") == 0 ||
                       JLI_StrCmp(arg, "-h") == 0 ||
                       JLI_StrCmp(arg, "-?") == 0) {
                printUsage = JNI_TRUE;
                return JNI_TRUE;
            } else if (JLI_StrCmp(arg, "-version") == 0) {
                printVersion = JNI_TRUE;
                return JNI_TRUE;
            } else if (JLI_StrCmp(arg, "-showversion") == 0) {
                showVersion = JNI_TRUE;
            } else if (JLI_StrCmp(arg, "-X") == 0) {
                printXUsage = JNI_TRUE;
                return JNI_TRUE;
    /*
     * The following case checks for -XshowSettings OR -XshowSetting:SUBOPT.
     * In the latter case, any SUBOPT value not recognized will default to "all"
     */
            } else if (JLI_StrCmp(arg, "-XshowSettings") == 0 ||
                    JLI_StrCCmp(arg, "-XshowSettings:") == 0) {
                showSettings = arg;
            } else if (JLI_StrCmp(arg, "-Xdiag") == 0) {
                AddOption("-Dsun.java.launcher.diag=true", NULL);
    /*
     * The following case provide backward compatibility with old-style
     * command line options.
     */
            } else if (JLI_StrCmp(arg, "-fullversion") == 0) {
                JLI_ReportMessage("%s full version \"%s\"", _launcher_name, GetFullVersion());
                return JNI_FALSE;
            } else if (JLI_StrCmp(arg, "-verbosegc") == 0) {
                AddOption("-verbose:gc", NULL);
            } else if (JLI_StrCmp(arg, "-t") == 0) {
                AddOption("-Xt", NULL);
            } else if (JLI_StrCmp(arg, "-tm") == 0) {
                AddOption("-Xtm", NULL);
            } else if (JLI_StrCmp(arg, "-debug") == 0) {
                AddOption("-Xdebug", NULL);
            } else if (JLI_StrCmp(arg, "-noclassgc") == 0) {
                AddOption("-Xnoclassgc", NULL);
            } else if (JLI_StrCmp(arg, "-Xfuture") == 0) {
                AddOption("-Xverify:all", NULL);
            } else if (JLI_StrCmp(arg, "-verify") == 0) {
                AddOption("-Xverify:all", NULL);
            } else if (JLI_StrCmp(arg, "-verifyremote") == 0) {
                AddOption("-Xverify:remote", NULL);
            } else if (JLI_StrCmp(arg, "-noverify") == 0) {
                AddOption("-Xverify:none", NULL);
            } else if (JLI_StrCCmp(arg, "-prof") == 0) {
                char *p = arg + 5;
                char *tmp = JLI_MemAlloc(JLI_StrLen(arg) + 50);
                if (*p) {
                    sprintf(tmp, "-Xrunhprof:cpu=old,file=%s", p + 1);
                } else {
                    sprintf(tmp, "-Xrunhprof:cpu=old,file=java.prof");
                }
                AddOption(tmp, NULL);
            } else if (JLI_StrCCmp(arg, "-ss") == 0 ||
                       JLI_StrCCmp(arg, "-oss") == 0 ||
                       JLI_StrCCmp(arg, "-ms") == 0 ||
                       JLI_StrCCmp(arg, "-mx") == 0) {
                char *tmp = JLI_MemAlloc(JLI_StrLen(arg) + 6);
                sprintf(tmp, "-X%s", arg + 1); /* skip '-' */
                AddOption(tmp, NULL);
            } else if (JLI_StrCmp(arg, "-checksource") == 0 ||
                       JLI_StrCmp(arg, "-cs") == 0 ||
                       JLI_StrCmp(arg, "-noasyncgc") == 0) {
                /* No longer supported */
                JLI_ReportErrorMessage(ARG_WARN, arg);
            } else if (JLI_StrCCmp(arg, "-version:") == 0 ||
                       JLI_StrCmp(arg, "-no-jre-restrict-search") == 0 ||
                       JLI_StrCmp(arg, "-jre-restrict-search") == 0 ||
                       JLI_StrCCmp(arg, "-splash:") == 0) {
                ; /* Ignore machine independent options already handled */
            } else if (ProcessPlatformOption(arg)) {
                ; /* Processing of platform dependent options */
            } else if (RemovableOption(arg)) {
                ; /* Do not pass option to vm. */
            } else {
                AddOption(arg, NULL);
            }
        }
    
        if (--argc >= 0) {
            *pwhat = *argv++;
        }
    
        if (*pwhat == NULL) {
            *pret = 1;
        } else if (mode == LM_UNKNOWN) {
            /* default to LM_CLASS if -jar and -cp option are
             * not specified */
            mode = LM_CLASS;
        }
    
        if (argc >= 0) {
            *pargc = argc;
            *pargv = argv;
        }
    
        *pmode = mode;
    
        return JNI_TRUE;
    }
    
    • 对一些参数做了校验,比如-classpath、-cp和-jar选项后面还需要有参数;
    • AddOption函数将选项添加到虚拟机的启动选项中,如-D指定的属性、-X系列参数和-XX系列参数。它特殊处理了-Xss、-Xmx和-Xms这三个选项,不会把它们加到启动选项中,而是会将后面的值分别保存到全局变量threadStackSize、maxHeapSize和initialHeapSize供后续启动虚拟机使用;
    • 将一些参数转换成新的形式后再调用AddOption函数,比如-ms、-mx选项会分别被转换成-Xms和-Xmx,而-noverify会被转换成-Xverify:none;
    • pwhat指向了命令选项之后的第一个操作数(比如java命令的主类或者-jar选项后跟的jar包),如果没有是不会报错的;
    • pmode表示启动模式,LM_CLASS或LM_JAR;
    • 由于参数pargc和pargv都是指针,因此返回到JLI_Launch函数后argv就只剩操作数后面传递给操作数(或者叫做应用)的参数了。

    ParseArguments函数的重点在while循环,循环条件决定了只处理以连字符开头的选项,如果命令形如java -jar xxx.jar -version -Xmx64m,那么遇到xxx.jar时就会跳出循环,导致后面的两个选项不会被处理而被当成运行jar时传给jar的命令行参数。因此使用相关命令时各个选项的顺序很重要。
    另外需要注意JAVA_ARGS宏的影响,以javac为例,对javac -g Test.java来说,第一个操作数就是com.sun.tools.javac.Main(还是得注意while循环的退出条件),在函数返回后argv是-g Test.java,在这点上JDK工具与一般的情况不同。

    设置伪参数

    SetJavaCommandLineProp(what, argc, argv);
    SetJavaLauncherProp();
    SetJavaLauncherPlatformProps();
    
    • SetJavaCommandLineProp函数添加新的虚拟机启动参数:-Dsun.java.command=<第一个操作数> <传给操作数的参数列表>;
    • SetJavaLauncherProp函数添加新的虚拟机启动参数:-Dsun.java.launcher=SUN_STANDARD;
    • SetJavaLauncherPlatformProps函数添加新的虚拟机启动参数:-Dsun.java.launcher.pid=进程标识符。

    初始化JVM

    命令行参数被解析后,虚拟机启动选项已经构造完毕,JLI_Launch函数最后调用JVMInit函数在新的线程中初始化虚拟机,在新线程做的原因详见https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6316197。JVMInit函数定义在文件jdk/src/solaris/bin/java_md_solinux.c中,相关代码如下:

    int
    JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
            int argc, char **argv,
            int mode, char *what, int ret)
    {
        ShowSplashScreen();
        return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
    }
    
    • ifn结构体保存了先前用LoadJavaVM函数在JVM动态库中查找到的JNI_CreateJavaVM、JNI_GetDefaultJavaVMInitArgs和JNI_GetCreatedJavaVMs函数指针。
    int
    ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
                        int argc, char **argv,
                        int mode, char *what, int ret)
    {
    
        /*
         * If user doesn't specify stack size, check if VM has a preference.
         * Note that HotSpot no longer supports JNI_VERSION_1_1 but it will
         * return its default stack size through the init args structure.
         */
        if (threadStackSize == 0) {
          struct JDK1_1InitArgs args1_1;
          memset((void*)&args1_1, 0, sizeof(args1_1));
          args1_1.version = JNI_VERSION_1_1;
          ifn->GetDefaultJavaVMInitArgs(&args1_1);  /* ignore return value */
          if (args1_1.javaStackSize > 0) {
             threadStackSize = args1_1.javaStackSize;
          }
        }
    
        { /* 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;
        }
    }
    

    ContinueInNewThread函数的参数分别是:

    • ifn保存了JVM动态库的函数指针;
    • argc和argv分别是传递给第一个操作数的参数个数和参数列表;
    • mode是启动模式,从类启动还是从jar启动;
    • what是第一个操作数。
    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;
        } 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);
        return rslt;
    }
    

    在ContinueInNewThread0中,父线程首先利用pthread库函数创建新线程执行JavaMain函数的代码,然后调用pthread_join函数将自己阻塞。

    相关文章

      网友评论

          本文标题:Java Hotspot虚拟机的启动过程(二)

          本文链接:https://www.haomeiwen.com/subject/bvzfsqtx.html