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