JVM源码分析(一) -- java启动流程
准备工作
首先需要下好jdk源码,我选择的是jdk8,编译过程略去。
经过configure,make之后。在jdk8u-dev/build/macosx-x86_64-normal-server-release/jdk/bin目录会生成可执行文件。java,javac,jstack等工具,这个过程花了十几分钟。
先准备一份java编写的程序用作调试,编写HelloWorld.java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
public class HelloWorld {
public HelloWorld() {
}
public static void main(String[] args) {
System.out.println("HelloWorld!");
}
}
编成class
localhost:tmp jjchen$ javac HelloWorld.java
编写 MANIFEST.MF文件确定程序执行入口,将helloworld打成jar包
// file META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: 1.8.0_171 (Oracle Corporation)
Main-Class: HelloWorld
localhost:tmp jjchen$ jar cvfm HelloWorld.jar META-INF/MANIFEST.MF HelloWorld.class //注意MF文件要放到前面,否则打包不了
流程分析
查看源码分析一下运行java程序之后做了什么事。命令行
./java -jar -Xms3550m -XstartOnFirstThread HelloWorld.jar
程序入口在jdk8u-dev/jdk/src/share/bin/main.c
,查看main函数。
#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);
}
非win编译环境,什么都没干,查看JLI_Launch函数,位于jdk8u-dev/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 */
)
{
int mode = LM_UNKNOWN;
char *what = NULL;
char *cpath = 0;
char *main_class = NULL;
int ret;
InvocationFunctions ifn;
jlong start, end;
char jvmpath[MAXPATHLEN];
char jrepath[MAXPATHLEN];
char jvmcfg[MAXPATHLEN];
_fVersion = fullversion;
_dVersion = dotversion;
_launcher_name = lname;
_program_name = pname;
_is_java_args = javaargs;
_wc_enabled = cpwildcard;
_ergo_policy = ergo;
InitLauncher(javaw);
DumpState();
if (JLI_IsTraceLauncher()) {
int i;
printf("Command line args:\n");
for (i = 0; i < argc ; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
AddOption("-Dsun.java.launcher.diag=true", NULL);
}
/*
* 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);
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
jvmcfg, sizeof(jvmcfg));
if (!IsJavaArgs()) {
SetJvmEnvironment(argc,argv);
}
ifn.CreateJavaVM = 0;
ifn.GetDefaultJavaVMInitArgs = 0;
if (JLI_IsTraceLauncher()) {
start = CounterGet();
}
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
if (JLI_IsTraceLauncher()) {
end = CounterGet();
}
JLI_TraceLauncher("%ld micro seconds to LoadJavaVM\n",
(long)(jint)Counter2Micros(end-start));
++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 */
}
/* set the -Dsun.java.command pseudo property */
SetJavaCommandLineProp(what, argc, argv);
/* Set the -Dsun.java.launcher pseudo property */
SetJavaLauncherProp();
/* set the -Dsun.java.launcher.* platform properties */
SetJavaLauncherPlatformProps();
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
我们需要特别关心以下这几个步骤
1.SelectVersion
解析部分命令行参数,主要是判断用户是以jar包方式运行(-jar)还是以class方式运行,并读取路径(-classpath/-cp),默认为环境变量配置的当前目录。如果是以jar包方式加载的,那么还会去读取META-INF/MANIFEST.MF文件,读入以下参数:Manifest-Version
,Main-Class
,JRE-Version
,JRE-Restrict-Search
,Splashscreen-Image
,除了Main-Class
是程序入口以外,JRE-Restrict-Search
是jvm的查找路径,其他含义先不了解了。
2.CreateExecutionEnvironment
-
1. ReadKnownVMs函数
获取jvm.cfg的一些参数 -
2. MacOSXStartup函数
开启了一个线程重新执行了main方法,同时将主线程挂起(。。。这个是干嘛)
4.LoadJavaVM
新线程继承主线程的遗志,加载jvm动态库,并加载了该动态库导出的三个方法,JNI_CreateJavaVM
,JNI_GetDefaultJavaVMInitArgs
,JNI_GetCreatedJavaVMs
。这个比较重要,贴一下代码。
jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
Dl_info dlinfo;
void *libjvm;
JLI_TraceLauncher("JVM path is %s\n", jvmpath);
libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
if (libjvm == NULL) {
JLI_ReportErrorMessage(DLL_ERROR1, __LINE__);
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
ifn->CreateJavaVM = (CreateJavaVM_t)
dlsym(libjvm, "JNI_CreateJavaVM");
if (ifn->CreateJavaVM == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
if (ifn->GetDefaultJavaVMInitArgs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
dlsym(libjvm, "JNI_GetCreatedJavaVMs");
if (ifn->GetCreatedJavaVMs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
return JNI_TRUE;
}
5.ParseArguments
解析用户输入参数,最终调用AddOption方法添加一些option参数保存起来,jvm堆栈大小的配置就是这里保存的。设置初始 Java 堆大小:-Xms512m
,最后的单位可以是g/t/m/k
。
同样的,前缀Xmx
是设置最大的堆内存,Xss
是设置栈空间大小。
void
AddOption(char *str, void *info)
{
/*
* Expand options array if needed to accommodate at least one more
* VM option.
*/
if (numOptions >= maxOptions) {
if (options == 0) {
maxOptions = 4;
options = JLI_MemAlloc(maxOptions * sizeof(JavaVMOption));
} else {
JavaVMOption *tmp;
maxOptions *= 2;
tmp = JLI_MemAlloc(maxOptions * sizeof(JavaVMOption));
memcpy(tmp, options, numOptions * sizeof(JavaVMOption));
JLI_MemFree(options);
options = tmp;
}
}
options[numOptions].optionString = str;
options[numOptions++].extraInfo = info;
if (JLI_StrCCmp(str, "-Xss") == 0) {
jlong tmp;
if (parse_size(str + 4, &tmp)) {
threadStackSize = tmp;
}
}
if (JLI_StrCCmp(str, "-Xmx") == 0) {
jlong tmp;
if (parse_size(str + 4, &tmp)) {
maxHeapSize = tmp;
}
}
if (JLI_StrCCmp(str, "-Xms") == 0) {
jlong tmp;
if (parse_size(str + 4, &tmp)) {
initialHeapSize = tmp;
}
}
}
6.JVM初始化及运行
先是初始化:
int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret) {
if (sameThread) {
JLI_TraceLauncher("In same thread\n");
// need to block this thread against the main thread
// so signals get caught correctly
__block int rslt = 0;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
{
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock: ^{
JavaMainArgs args;
args.argc = argc;
args.argv = argv;
args.mode = mode;
args.what = what;
args.ifn = *ifn;
rslt = JavaMain(&args);
}];
/*
* We cannot use dispatch_sync here, because it blocks the main dispatch queue.
* Using the main NSRunLoop allows the dispatch queue to run properly once
* SWT (or whatever toolkit this is needed for) kicks off it's own NSRunLoop
* and starts running.
*/
[op performSelectorOnMainThread:@selector(start) withObject:nil waitUntilDone:YES];
}
[pool drain];
return rslt;
} else {
return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
}
正常情况下是调用ContinueInNewThread
,然后创建一个新的线程调用java.c
的JavaMain
函数。 当然,如果我们在启动参数里面加了-XstartOnFirstThread
, sameThread会被置位,唤醒主线程来调用JavaMain
(这段代码是什么语法没看懂)。
我们进一步查看JavaMain的实现。
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);
}
if (showSettings != NULL) {
ShowSettings(env, showSettings);
CHECK_EXCEPTION_LEAVE(1);
}
if (printVersion || showVersion) {
PrintJavaVersion(env, showVersion);
CHECK_EXCEPTION_LEAVE(0);
if (printVersion) {
LEAVE();
}
}
/* If the user specified neither a class name nor a JAR file */
if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
PrintUsage(env, printXUsage);
CHECK_EXCEPTION_LEAVE(1);
LEAVE();
}
FreeKnownVMs(); /* after last possible PrintUsage() */
if (JLI_IsTraceLauncher()) {
end = CounterGet();
JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n",
(long)(jint)Counter2Micros(end-start));
}
/* At this stage, argc/argv have the application's arguments */
if (JLI_IsTraceLauncher()){
int i;
printf("%s is '%s'\n", launchModeNames[mode], what);
printf("App's argc is %d\n", argc);
for (i=0; 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).
*
* 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);
CHECK_EXCEPTION_LEAVE(1);
/*
* 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();
}
6.1 创建虚拟机
InitializeJVM
调用JNI_CreateJavaVM
,传入初始参数创建一个虚拟机,并返回了一个JNIEnv
对象。JNIEnv类定义在jdk8u-dev/hotspot/src/share/vm/prims/jni.h
中。CreateJavaVM的实现在对应的jni.cpp
中,
粘贴一下实现
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
#ifndef USDT2
HS_DTRACE_PROBE3(hotspot_jni, CreateJavaVM__entry, vm, penv, args);
#else /* USDT2 */
HOTSPOT_JNI_CREATEJAVAVM_ENTRY(
(void **) vm, penv, args);
#endif /* USDT2 */
jint result = JNI_ERR;
DT_RETURN_MARK(CreateJavaVM, jint, (const jint&)result);
// We're about to use Atomic::xchg for synchronization. Some Zero
// platforms use the GCC builtin __sync_lock_test_and_set for this,
// but __sync_lock_test_and_set is not guaranteed to do what we want
// on all architectures. So we check it works before relying on it.
#if defined(ZERO) && defined(ASSERT)
{
jint a = 0xcafebabe;
jint b = Atomic::xchg(0xdeadbeef, &a);
void *c = &a;
void *d = Atomic::xchg_ptr(&b, &c);
assert(a == (jint) 0xdeadbeef && b == (jint) 0xcafebabe, "Atomic::xchg() works");
assert(c == &b && d == &a, "Atomic::xchg_ptr() works");
}
#endif // ZERO && ASSERT
// At the moment it's only possible to have one Java VM,
// since some of the runtime state is in global variables.
// We cannot use our mutex locks here, since they only work on
// Threads. We do an atomic compare and exchange to ensure only
// one thread can call this method at a time
// We use Atomic::xchg rather than Atomic::add/dec since on some platforms
// the add/dec implementations are dependent on whether we are running
// on a multiprocessor, and at this stage of initialization the os::is_MP
// function used to determine this will always return false. Atomic::xchg
// does not have this problem.
if (Atomic::xchg(1, &vm_created) == 1) {
return JNI_EEXIST; // already created, or create attempt in progress
}
if (Atomic::xchg(0, &safe_to_recreate_vm) == 0) {
return JNI_ERR; // someone tried and failed and retry not allowed.
}
assert(vm_created == 1, "vm_created is true during the creation");
/**
* Certain errors during initialization are recoverable and do not
* prevent this method from being called again at a later time
* (perhaps with different arguments). However, at a certain
* point during initialization if an error occurs we cannot allow
* this function to be called again (or it will crash). In those
* situations, the 'canTryAgain' flag is set to false, which atomically
* sets safe_to_recreate_vm to 1, such that any new call to
* JNI_CreateJavaVM will immediately fail using the above logic.
*/
bool can_try_again = true;
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);
}
EventThreadStart event;
if (event.should_commit()) {
event.set_javalangthread(java_lang_Thread::thread_id(thread->threadObj()));
event.commit();
}
#ifndef PRODUCT
#ifndef CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED
#define CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(f) f()
#endif
// Check if we should compile all classes on bootclasspath
if (CompileTheWorld) ClassLoader::compile_the_world();
if (ReplayCompiles) ciReplay::replay(thread);
// Some platforms (like Win*) need a wrapper around these test
// functions in order to properly handle error conditions.
CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(test_error_handler);
CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(execute_internal_vm_tests);
#endif
// Since this is not a JVM_ENTRY we have to set the thread state manually before leaving.
ThreadStateTransition::transition_and_fence(thread, _thread_in_vm, _thread_in_native);
} else {
if (can_try_again) {
// reset safe_to_recreate_vm to 1 so that retrial would be possible
safe_to_recreate_vm = 1;
}
// Creation failed. We must reset vm_created
*vm = 0;
*(JNIEnv**)penv = 0;
// reset vm_created last to avoid race condition. Use OrderAccess to
// control both compiler and architectural-based reordering.
OrderAccess::release_store(&vm_created, 0);
}
return result;
}
JNI_CreateJavaVM
有效调用只有一次,已经创建过了会直接返回。
最重要的是这句:Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
,函数体中初始化了vm环境,这个比较庞杂,后续分析。
6.2
- 根据MANIFEST.MF中配置的Main-Class查找出对应的类。
- 根据对应的类找到main静态方法。
- 调用之。
可以看到,在调用完之后,"HelloWorld!"字样就打印出来啦。但是实际上这3个步骤是jvm动态库实现的,里面的逻辑瞄了一眼异常复杂,后续分析。
总结
总结一下,java程序 = java空壳子+jvm;后续开始分析jvm的功能。