Java虚拟机的启动与程序的执行

时间:2022-04-17 04:49:28

这篇文章是从 OpenJDK 源码的角度讲当我们执行了

java -classpath . hello

之后,java.exe 怎样从 main 函数開始运行,启动虚拟机,并运行字节码中的代码。

实验环境

要了解一个系统是怎样执行的,光看是不行的,要实际地执行,调试,改动才干对系统的动作方式有所了解。

起初我是依照 GitHub 上的一个项目 OpenJDK-Research 在 windows 7 64位平台上,使用 Visual Studio 2010 来调试,执行的。可是后来发现,这个项目只编译了HotSpot虚拟机, java.exe 并没有编译。

这里我们首先弄明确 java.exe 和虚拟机之间的关系。我们使用 Visual Studio 编译出的 HotSpot 是虚拟机,是作为动态链接库的形式被 java.exe 载入的。java.exe 负责解析參数,载入虚拟机链接库,它须要调用虚拟机中的函数来完毕运行
Java 程序的功能。所以,你在HotSpot的源码中找不到启动的程序的 main 函数,本来在 openjdk7 中,虚拟机是带有一个启动器的,在文件夹 openjdk/hotspot/src/share/tools/launcher/java.c 中能够找到
main 函数,可是在 openjdk8 中,这个启动器不见了,被放在 openjdk/jdk 文件夹下,而不是 openjdk/hotspot 文件夹下了,给我们的学习过程造成了伤害。

所以我后来就在 linux 平台上调试了,由于在 windows 平台上,我始终没有把整个 openjdk8 编译成功,编译不出java.exe, 只编译了 hotspot,是看不到从
main 函数開始的运行的。关于怎样在 linux 平台下编译调试 openjdk8,能够參考我的还有一篇文章 在Ubuntu 12.04 上编译 openjdk8.

调用栈

jdk8u/jdk/src/share/bin/main.c::WinMain/main
jdk8u/jdk/src/share/bin/java.c::JLI_Launch
jdk8u/jdk/src/solaris/bin/java_md_solinux.c::LoadJavaVM # Load JVM Library: libjvm.so
jdk8u/jdk/src/solaris/bin/java_md_solinux.c::JVMInit # Create JVM
jdk8u/jdk/src/share/bin/java.c::ContinueInNewThread
jdk8u/jdk/src/solaris/bin/java_md_solinux.c::ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args)
jdk8u/jdk/src/share/bin/java.c::JavaMain
jdk8u/jdk/src/share/bin/java.c::InitializeJVM
jdk8u\hotspot\src\share\vm\prims\jni.cpp::JNI_CreateJavaVM

运行过程

  • main.c (jdk8u/jdk/src/share/bin/main.c)
#ifdef JAVAW

char **__initenv;

int WINAPI
WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow)
{
int margc;
char** margv;
const jboolean const_javaw = JNI_TRUE; __initenv = _environ;
#else /* JAVAW */
int
main(int argc, char **argv)
{
int margc;
char** margv;
const jboolean const_javaw = JNI_FALSE;
#endif /* JAVAW */
#ifdef _WIN32
{
int i = 0;
if (getenv(JLDEBUG_ENV_ENTRY) != NULL) {
printf("Windows original main args:\n");
for (i = 0 ; i < __argc ; i++) {
printf("wwwd_args[%d] = %s\n", i, __argv[i]);
}
}
}
JLI_CmdToArgs(GetCommandLine());
margc = JLI_GetStdArgc();
// add one more to mark the end
margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *)));
{
int i = 0;
StdArg *stdargs = JLI_GetStdArgs();
for (i = 0 ; i < margc ; i++) {
margv[i] = stdargs[i].arg;
}
margv[i] = NULL;
}
#else /* *NIXES */
margc = argc;
margv = argv;
#endif /* WIN32 */
return JLI_Launch(margc, margv,
sizeof(const_jargs) / sizeof(char *), const_jargs,
sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
FULL_VERSION,
DOT_VERSION,
(const_progname != NULL) ? const_progname : *margv,
(const_launcher != NULL) ? const_launcher : *margv,
(const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
const_cpwildcard, const_javaw, const_ergo_class);
}

这就是传说中的 main 函数的真身,能够看出,它针对操作系统是否使用 Windows ,运行了不同的代码段,终于调用JLI_Launch 函数。

  • JLI_Lanuch(jdk8u/jdk/src/share/bin/java.c)
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 */
)
{ ... if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
} ... return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret); }

从这里能够看出 JLI_Lanuch 的各个參数的含义, 我列出了关键代码, 当中 LoadJavaVM 完毕加载虚拟机动态链接库,并初始化 ifn 中的函数指针,HotSpot虚拟机就是这样向启动器 java 提供功能。

  • LoadJavaVM (jdk8u/jdk/src/solaris/bin/java_md_solinux.c)

这个函数涉及动态链接库,不同操作系统有不同接口,这里是针对 linux 的。

jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
... libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL); ... ifn->CreateJavaVM = (CreateJavaVM_t)
dlsym(libjvm, "JNI_CreateJavaVM"); ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs"); ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
dlsym(libjvm, "JNI_GetCreatedJavaVMs"); ...

从这里能够看出加载动态链接库以及初始化 ifn 数据结构的代码。在我的调试版本号中,javapath 指向之前编译出的动态链接库 jdk8u/build/fastdebug/jdk/lib/i386/server/libjvm.so.

  • JVM_Init(jdk8u/jdk/src/solaris/bin/java_md_solinux.c)

回到 JLI_Lanuch 函数,我们终于进入 JVM_Init,
这个函数会启动一个新线程。

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);
}

ContinueInNewThread 会调用还有一个函数 ContinueInNewThread0 启动线程,运行 JavaMain 函数:

int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) { ... 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);
} ...
  • JavaMain(jdk8u/jdk/src/share/bin/java.c)

这个函数会初始化虚拟机,载入各种类,并运行应用程序中的 main 函数。凝视非常具体。

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(); /* Initialize the virtual machine */
start = CounterGet();
if (!InitializeJVM(&vm, &env, &ifn)) {
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
} ... 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);
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);
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);
/*
* 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.
*/
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
CHECK_EXCEPTION_NULL_LEAVE(mainID); /* Build platform specific argument array */
mainArgs = CreateApplicationArgs(env, argv, argc);
CHECK_EXCEPTION_NULL_LEAVE(mainArgs); /* 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;
LEAVE();
}

注意 InitializeJVM 函数,它会调用之前初始化的 ifn 数据结构中的 CreateJavaVM 函数.

  • InitializeJVM(jdk8u/jdk/src/share/bin/java.c::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);
JLI_MemFree(options);
return r == JNI_OK;
}

ifn->CreateJavaVM指向虚拟机动态链接库中的 JNI_CreateJavaVM 函数,这个函数会真正创建虚拟机。
这个函数运行后,pvm, penv 的值就会被设定,我们能够比較下运行前后它们的值,来看看它们的作用。

// before r = ifn->CreateJavaVM(pvm, (void **)penv, &args);

(gdb) p *pvm
$8 = (JavaVM *) 0x0
(gdb) p *penv
$9 = (JNIEnv *) 0x0
// after r = ifn->CreateJavaVM(pvm, (void **)penv, &args);

(gdb) p ***penv
$14 = {reserved0 = 0x0, reserved1 = 0x0, reserved2 = 0x0, reserved3 = 0x0,
GetVersion = 0xb6ede599 <jni_GetVersion>,
DefineClass = 0xb6eb20a0 <jni_DefineClass>,
FindClass = 0xb6eb253c <jni_FindClass>,
FromReflectedMethod = 0xb6eb2b17 <jni_FromReflectedMethod>,
FromReflectedField = 0xb6eb2edb <jni_FromReflectedField>,
...
...
} (gdb) p ***pvm
$15 = {reserved0 = 0x0, reserved1 = 0x0, reserved2 = 0x0,
DestroyJavaVM = 0xb6edf1e8 <jni_DestroyJavaVM>,
AttachCurrentThread = 0xb6edf69a <jni_AttachCurrentThread>,
DetachCurrentThread = 0xb6edf795 <jni_DetachCurrentThread>,
GetEnv = 0xb6edf8d3 <jni_GetEnv>,
AttachCurrentThreadAsDaemon = 0xb6edfa7d <jni_AttachCurrentThreadAsDaemon>}

能够看出它们得到了hotspot 中以 jni_ 开头的一些函数,虚拟机正是以这种方式向外提供功能。我们大概看一下JNI_CreateJavaVM 的功能。


  • JNI_CreateJavaVM(jdk8u\hotspot\src\share\vm\prims\jni.cpp)
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
... result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
if (result == JNI_OK) {
JavaThread *thread = JavaThread::current();
/* thread is thread_in_vm here */
*vm = (JavaVM *)(&main_vm);
*(JNIEnv**)penv = thread->jni_environment(); // Tracks the time application was running before GC
RuntimeService::record_application_start(); // Notify JVMTI
if (JvmtiExport::should_post_thread_life()) {
JvmtiExport::post_thread_start(thread);
} ... } ... }

当中的 create_vm 函数是虚拟机初始化的关键,它初始化了虚拟机的大部分组件。另外能够看到
vm, penv 的值被设定。

这个函数位于 jdk8u\hotspot\src\share\vm\prims\jni.cpp

我之前在 Windows 下调试,直接调试的 HotSpot 动态链接库,能够看到的第一个函数就是 JNI_CreateJavaVM, 之前的调用都位于 java.exe 代码中。由于
Windows 中 java.exe 不是我们自己编译的,看不到当中调用关系。例如以下图所看到的:

Java虚拟机的启动与程序的执行

同一时候能够看到两个线程

Java虚拟机的启动与程序的执行