这篇文章是从 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
不是我们自己编译的,看不到当中调用关系。例如以下图所看到的:
同一时候能够看到两个线程