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

本文接着前一篇文章继续分析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函数将自己阻塞。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容