深入Java虚拟机笔记(五):剖析HotSpot的Launcher

时间:2022-04-06 09:37:15

介绍

HotSpot属于OpenJDK项目的一个功能子集,HotSpot目录下四大子目录:

agent:包含Serviceability Agent的客户端的实现
make:用于build出HotSpot的各种配置文件
src:包括HotSpot的所有源码
test:单元测试

HotSpot VM源码目录结构

 
├─agent Serviceability Agent的实现
├─make 用来build出HotSpot的各种配置文件
├─src HotSpot VM的源代码
│ ├─cpu CPU相关代码
│ ├─os 操作系相关代码
│ ├─os_cpu 操作系统+CPU的组合相关的代码
│ └─share 平台无关的共通代码
│ ├─tools 工具
│ │ ├─hsdis 反汇编插件
│ │ ├─IdealGraphVisualizer 将server编译器的中间代码可视化的工具
│ │ ├─launcher 启动程序“java”
│ │ ├─LogCompilation 将-XX:+LogCompilation输出的日志(hotspot.log)整理成更容易阅读的格式的工具
│ │ └─ProjectCreator 生成Visual Studio的project文件的工具
│ └─vm HotSpot VM的核心代码
│ ├─adlc 平台描述文件(上面的cpu或os_cpu里的*.ad文件)的编译器
│ ├─asm 汇编器接口
│ ├─c1 client编译器
│ ├─ci 动态编译器的公共服务/接口
│ ├─classfile 类文件的处理(包括类加载和系统符号表等)
│ ├─code 动态生成的代码的管理
│ ├─compiler 编译器接口
│ ├─gc_implementation GC的实现
│ │ ├─concurrentMarkSweep Concurrent Mark Sweep GC的实现
│ │ ├─g1 Garbage-First GC的实现(不使用老的分代式GC框架)
│ │ ├─parallelScavenge ParallelScavenge GC的实现(server VM默认,不使用老的分代式GC框架)
│ │ ├─parNew ParNew GC的实现
│ │ └─shared GC的共通实现
│ ├─gc_interface GC的接口
│ ├─interpreter 解释器,包括“模板解释器”(官方版在用)和“C++解释器”(官方版不在用)
│ ├─libadt 一些抽象数据结构
│ ├─memory 内存管理相关(老的分代式GC框架也在这里)
│ ├─oops HotSpot VM的对象系统的实现
│ ├─opto server编译器
│ ├─prims HotSpot VM的对外接口,包括部分标准库的native部分和JVMTI实现
│ ├─runtime 运行时支持库(包括线程管理、编译器调度、锁、反射等)
│ ├─services 主要是用来支持JMX之类的管理功能的接口
│ ├─shark 基于LLVM的JIT编译器(官方版里没有使用)
│ └─utilities 一些基本的工具类
└─test 单元测试

Launcher是一直用于启动JVM进程的启动器,有两种,

一种windows平台下运行时会保留在控制台
一种用于执行Java的GUI程序,不会显示任何程序的输出信息

Launcher只是一个封装了虚拟机的执行外壳,由它负责装载JRE环境和windows平台下的jvm.dll动态链接库

Launcher的执行过程

深入Java虚拟机笔记(五):剖析HotSpot的Launcher

1、启动函数main()

(1)Launcher启动后,对与运行环境有关的局部变量进行初始化。该局部变量在后面创建运行环境需要用到,在调用JavaMain()函数也需要传递过去

    char *jarfile = 0;
char *classname = 0;
char *s = 0;
char *main_class = NULL;
int ret;
InvocationFunctions ifn;
jlong start, end;
char jrepath[MAXPATHLEN], jvmpath[MAXPATHLEN];
char ** original_argv = argv;

(2)创建运行环境

    CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
original_argv)
;

printf("Using java runtime at: %s\n", jrepath);

(3)在函数程序末尾,创建一个新的线程去执行JVM的初始化和正式调用Java程序main()方法

   struct JavaMainArgs args;

args.argc = argc;
args.argv = argv;
args.jarfile = jarfile;
args.classname = classname;
args.ifn = ifn;

return ContinueInNewThread(JavaMain, threadStackSize, (void*)&args);

2、在主线程中执行JavaMain()函数

main()函数完成运行环境的创建后,接下来就任务交给在主线程中执行JavaMain()函数,JavaMain()函数会从Main()函数中传递过来的一些变量参数,然后初始化几个比较重要的局部变量

//从Main()函数中传递过来的一些变量参数
struct JavaMainArgs *args = (struct JavaMainArgs *)_args;
int argc = args->argc;
char **argv = args->argv;
char *jarfile = args->jarfile;
char *classname = args->classname;
InvocationFunctions ifn = args->ifn;
//初始化几个比较重要的局部变量
JavaVM *vm = 0;
JNIEnv *env = 0;
jstring mainClassName;
jclass mainClass;
jmethodID mainID;
jobjectArray mainArgs;
int ret = 0;
jlong start, end;

结构体JavaMainArgs如下

struct JavaMainArgs {
int argc;
char ** argv;
char * jarfile;
char * classname;
InvocationFunctions ifn;
};

结构体JavaMainArgs内部的 InvocationFunctions类型中包含的函数指针与对应的目标函数,如下:

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

函数指针所指向的目标函数主要用于完成断开主线程和执行JVM销毁等功能,如下:

(*vm)->DetachCurrentThread(vm)
(*vm)->DestroyJavaVM(vm)

当函数指针成功指向目标函数和JVM初始化后,Launcher会执行LoadClass()函数和GetStaticMethodID()函数

//LoadClass()用于获取Java程序的启动类
mainClass = LoadClass(env, classname);
//GetStaticMethodID()用于获取Java程序的启动方法
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");

接下来,调用CallStaticVoidMethod()执行Java程序的Main()方法

 (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

当Java程序执行完后,断开与主线程的连接

  if ((*vm)->DetachCurrentThread(vm) != 0) {
message = "Could not detach main thread.";
messageDest = JNI_TRUE;
ret = 1;
goto leave;
}int

当断开连接后,Launcher等待所有非守护线程全部执行完成,最后销毁JVM

 (*vm)->DestroyJavaVM(vm);

附上相关源码

Main()函数全部源码

int main(int argc, char ** argv)
{
char *jarfile = 0;
char *classname = 0;
char *s = 0;
char *main_class = NULL;
int ret;
InvocationFunctions ifn;
jlong start, end;
char jrepath[MAXPATHLEN], jvmpath[MAXPATHLEN];
char ** original_argv = argv;

if (getenv("_JAVA_LAUNCHER_DEBUG") != 0) {
_launcher_debug = JNI_TRUE;
printf("----_JAVA_LAUNCHER_DEBUG----\n");
}

#ifndef GAMMA
/*
* 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);
#endif /* ifndef GAMMA */

/* copy original argv */
{
int i;
original_argv = (char**)JLI_MemAlloc(sizeof(char*)*(argc+1));
for(i = 0; i < argc+1; i++)
original_argv[i] = argv[i];
}

CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
original_argv);

printf("Using java runtime at: %s\n", jrepath);

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

if (_launcher_debug)
start = CounterGet();
if (!LoadJavaVM(jvmpath, &ifn)) {
exit(6);
}
if (_launcher_debug) {
end = CounterGet();
printf("%ld micro seconds to LoadJavaVM\n",
(long)(jint)Counter2Micros(end-start));
}

#ifdef JAVA_ARGS /* javac, jar and friends. */
progname = "java";
#else /* java, oldjava, javaw and friends */
#ifdef PROGNAME
progname = PROGNAME;
#else
progname = *argv;
if ((s = strrchr(progname, FILE_SEPARATOR)) != 0) {
progname = s + 1;
}
#endif /* PROGNAME */
#endif /* JAVA_ARGS */
++argv;
--argc;

#ifdef JAVA_ARGS
/* Preprocess wrapper arguments */
TranslateApplicationArgs(&argc, &argv);
if (!AddApplicationOptions()) {
exit(1);
}
#endif

/* Set default CLASSPATH */
if ((s = getenv("CLASSPATH")) == 0) {
s = ".";
}
#ifndef JAVA_ARGS
SetClassPath(s);
#endif

/*
* Parse command line options; if the return value of
* ParseArguments is false, the program should exit.
*/
if (!ParseArguments(&argc, &argv, &jarfile, &classname, &ret, jvmpath)) {
exit(ret);
}

/* Override class path if -jar flag was specified */
if (jarfile != 0) {
SetClassPath(jarfile);
}

/* set the -Dsun.java.command pseudo property */
SetJavaCommandLineProp(classname, jarfile, argc, argv);

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

/* set the -Dsun.java.launcher.* platform properties */
SetJavaLauncherPlatformProps();

#ifndef GAMMA
/* Show the splash screen if needed */
ShowSplashScreen();
#endif

/*
* Done with all command line processing and potential re-execs so
* clean up the environment.
*/
(void)UnsetEnv(ENV_ENTRY);
#ifndef GAMMA
(void)UnsetEnv(SPLASH_FILE_ENV_ENTRY);
(void)UnsetEnv(SPLASH_JAR_ENV_ENTRY);

JLI_MemFree(splash_jar_entry);
JLI_MemFree(splash_file_entry);
#endif

/*
* 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 */
struct JavaMainArgs args;

args.argc = argc;
args.argv = argv;
args.jarfile = jarfile;
args.classname = classname;
args.ifn = ifn;

return
ContinueInNewThread(JavaMain, threadStackSize, (void*)&args);
}
}

JavaMain()函数全部源码

JavaMain(void * _args)
{
struct JavaMainArgs *args = (struct JavaMainArgs *)_args;
int argc = args->argc;
char **argv = args->argv;
char *jarfile = args->jarfile;
char *classname = args->classname;
InvocationFunctions ifn = args->ifn;

JavaVM *vm = 0;
JNIEnv *env = 0;
jstring mainClassName;
jclass mainClass;
jmethodID mainID;
jobjectArray mainArgs;
int ret = 0;
jlong start, end;

/*
* Error message to print or display; by default the message will
* only be displayed in a window.
*/
char * message = "Fatal exception occurred. Program will exit.";
jboolean messageDest = JNI_FALSE;

/* Initialize the virtual machine */

if (_launcher_debug)
start = CounterGet();
if (!InitializeJVM(&vm, &env, &ifn)) {
ReportErrorMessage("Could not create the Java virtual machine.",
JNI_TRUE);
exit(1);
}

if (printVersion || showVersion) {
PrintJavaVersion(env);
if ((*env)->ExceptionOccurred(env)) {
ReportExceptionDescription(env);
goto leave;
}
if (printVersion) {
ret = 0;
message = NULL;
goto leave;
}
if (showVersion) {
fprintf(stderr, "\n");
}
}

/* If the user specified neither a class name nor a JAR file */
if (jarfile == 0 && classname == 0) {
PrintUsage();
message = NULL;
goto leave;
}

#ifndef GAMMA
FreeKnownVMs(); /* after last possible PrintUsage() */
#endif

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

/* At this stage, argc/argv have the applications' arguments */
if (_launcher_debug) {
int i = 0;
printf("Main-Class is '%s'\n", classname ? classname : "");
printf("Apps' argc is %d\n", argc);
for (; i < argc; i++) {
printf(" argv[%2d] = '%s'\n", i, argv[i]);
}
}

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).
*/
if (jarfile != 0) {
mainClassName = GetMainClassName(env, jarfile);
if ((*env)->ExceptionOccurred(env)) {
ReportExceptionDescription(env);
goto leave;
}
if (mainClassName == NULL) {
const char * format = "Failed to load Main-Class manifest "
"attribute from\n%s";
message = (char*)JLI_MemAlloc((strlen(format) + strlen(jarfile)) *
sizeof(char))
;
sprintf(message, format, jarfile);
messageDest = JNI_TRUE;
goto leave;
}
classname = (char *)(*env)->
GetStringUTFChars(env, mainClassName, 0);
if (classname == NULL) {
ReportExceptionDescription(env);
goto leave;
}
mainClass = LoadClass(env, classname);
if(mainClass == NULL) { /* exception occured */
const char * format = "Could not find the main class: %s. Program will exit.";
ReportExceptionDescription(env);
message = (char *)JLI_MemAlloc((strlen(format) +
strlen(classname)) * sizeof(char) );
messageDest = JNI_TRUE;
sprintf(message, format, classname);
goto leave;
}
(*env)->ReleaseStringUTFChars(env, mainClassName, classname);
} else {
mainClassName = NewPlatformString(env, classname);
if (mainClassName == NULL) {
const char * format = "Failed to load Main Class: %s";
message = (char *)JLI_MemAlloc((strlen(format) + strlen(classname)) *
sizeof(char) );
sprintf(message, format, classname);
messageDest = JNI_TRUE;
goto leave;
}
classname = (char *)(*env)->GetStringUTFChars(env, mainClassName, 0);
if (classname == NULL) {
ReportExceptionDescription(env);
goto leave;
}
mainClass = LoadClass(env, classname);
if(mainClass == NULL) { /* exception occured */
const char * format = "Could not find the main class: %s. Program will exit.";
ReportExceptionDescription(env);
message = (char *)JLI_MemAlloc((strlen(format) +
strlen(classname)) * sizeof(char) );
messageDest = JNI_TRUE;
sprintf(message, format, classname);
goto leave;
}
(*env)->ReleaseStringUTFChars(env, mainClassName, classname);
}

/* Get the application's main method */
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
if (mainID == NULL) {
if ((*env)->ExceptionOccurred(env)) {
ReportExceptionDescription(env);
} else {
message = "No main method found in specified class.";
messageDest = JNI_TRUE;
}
goto leave;
}

{ /* Make sure the main method is public */
jint mods;
jmethodID mid;
jobject obj = (*env)->
ToReflectedMethod(env, mainClass,
mainID, JNI_TRUE);

if( obj == NULL) { /* exception occurred */
ReportExceptionDescription(env);
goto leave;
}

mid =
(*env)->
GetMethodID(env,
(*env)->GetObjectClass(env, obj),
"getModifiers", "()I");
if ((*env)->ExceptionOccurred(env)) {
ReportExceptionDescription(env);
goto leave;
}

mods = (*env)->
CallIntMethod(env, obj, mid);
if ((mods & 1) == 0) { /* if (!Modifier.isPublic(mods)) ... */
message = "Main method not public.";
messageDest = JNI_TRUE;
goto leave;
}
}

/* Build argument array */
mainArgs = NewPlatformStringArray(env, argv, argc);
if (mainArgs == NULL) {
ReportExceptionDescription(env);
goto leave;
}

/* Invoke main method. */
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

/*
* 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;

/*
* Detach the main thread so that it appears to have ended when
* the application'
s main method exits. This will invoke the
* uncaught exception handler machinery if main threw an
* exception. An uncaught exception handler cannot change the
* launcher's return code except by calling System.exit.
*/
if ((*vm)->DetachCurrentThread(vm) != 0) {
message = "Could not detach main thread.";
messageDest = JNI_TRUE;
ret = 1;
goto leave;
}int

message = NULL;

leave:
/*
* Wait for all non-daemon threads to end, then destroy the VM.
* This will actually create a trivial new Java waiter thread
* named "DestroyJavaVM", but this will be seen as a different
* thread from the one that executed main, even though they are
* the same C thread. This allows mainThread.join() and
* mainThread.isAlive() to work as expected.
*/
(*vm)->DestroyJavaVM(vm);

if(message != NULL && !noExitErrorMessage)
ReportErrorMessage(message, messageDest);
return ret;
}

参考:《Java虚拟机精讲》