JVM系列(二):jvm是如何加载java代码的?
上一篇粗略讲了下jvm的启动过程,但很多路子还没跑通。其中非常核心的,加载vm的过程。这个可以在hotspot中找到端倪。但jvm启动,又是如何载入java代码呢。
1. JavaMain加载流程
我们知道,java中入口是在main方法中,可以在命令行中指定main类,或者jar包中指定的manifest.xml中指定的main类。在java.c中,我们可以看到一个JavaMain方法,不知从何而来,但很像是直接加载java入口的方法。
// share/bin/java.c// 加载 main 函数类// 通过引入 JavaMain(), 接入java方法// #define JNICALL __stdcallint JNICALLJavaMain(void * _args){JavaMainArgs *args = (JavaMainArgs *)_args;int argc = args->argc;char **argv = args->argv;int mode = args->mode;char *what = args->what;// 一些jvm的调用实例,在之前的步骤中,通过加载相应动态链接方法,保存起来的/*** ifn->CreateJavaVM =* (void *)GetProcAddress(handle, "JNI_CreateJavaVM");* ifn->GetDefaultJavaVMInitArgs =* (void *)GetProcAddress(handle, "JNI_GetDefaultJavaVMInitArgs");*/InvocationFunctions ifn = args->ifn;JavaVM *vm = 0;JNIEnv *env = 0;jclass mainClass = NULL;jclass appClass = NULL; // actual application class being launchedjmethodID mainID;jobjectArray mainArgs;int ret = 0;jlong start, end;// collectorRegisterThread();/* Initialize the virtual machine */start = CounterGet();// 初始化jvm,失败则退出if (!InitializeJVM(&vm, &env, &ifn)) {JLI_ReportErrorMessage(JVM_ERROR1);exit(1);}// jvm检查完毕,如果只是一些展示类请求,则展示信息后,退出jvmif (showSettings != NULL) {ShowSettings(env, showSettings);/*** 宏是神奇的操作,此处 *env 直接引用#define CHECK_EXCEPTION_LEAVE(CEL_return_value) \do { \if ((*env)->ExceptionOccurred(env)) { \JLI_ReportExceptionDescription(env); \ret = (CEL_return_value); \LEAVE(); \} \} while (JNI_FALSE)*/CHECK_EXCEPTION_LEAVE(1);}// 调用 LEAVE() 方法的目的在于主动销毁jvm线程// 且退出当前方法调用,即 LEAVE() 后方法不再被执行/** Always 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.** 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.*//****#define LEAVE() \do { \if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { \JLI_ReportErrorMessage(JVM_ERROR2); \ret = 1; \} \if (JNI_TRUE) { \(*vm)->DestroyJavaVM(vm); \return ret; \} \} while (JNI_FALSE)*/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.*/// 加载 main 指定的class类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.*/// 加载main() 方法前执行初始化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.*/// 获取main()方法id, main(String[] args)mainID = (*env)->GetStaticMethodID(env, mainClass, "main","([Ljava/lang/String;)V");CHECK_EXCEPTION_NULL_LEAVE(mainID);/* Build platform specific argument array */// 构建args[] 参数mainArgs = CreateApplicationArgs(env, argv, argc);CHECK_EXCEPTION_NULL_LEAVE(mainArgs);/* Invoke main method. */// 调用java实现的main()方法// XX:: 重要实现(*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();}/** Loads a class and verifies that the main class is present and it is ok to* call it for more details refer to the java implementation.*/static jclassLoadMainClass(JNIEnv *env, int mode, char *name){jmethodID mid;jstring str;jobject result;jlong start, end;jclass cls = GetLauncherHelperClass(env);NULL_CHECK0(cls);if (JLI_IsTraceLauncher()) {start = CounterGet();}// checkAndLoadMain(String) 方法作为中间main()调用NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,"checkAndLoadMain","(ZILjava/lang/String;)Ljava/lang/Class;"));str = NewPlatformString(env, name);CHECK_JNI_RETURN_0(result = (*env)->CallStaticObjectMethod(env, cls, mid, USE_STDERR, mode, str));if (JLI_IsTraceLauncher()) {end = CounterGet();printf("%ld micro seconds to load main class\n",(long)(jint)Counter2Micros(end-start));printf("----%s----\n", JLDEBUG_ENV_ENTRY);}return (jclass)result;}// 初始化jvm, 主要是调用 CreateJavaVM() 方法,进行创建jvm操作/** Initializes the Java Virtual Machine. Also frees options array when* finished.*/static jbooleanInitializeJVM(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;}
略去核心加载jvm的实现,要加载main类,还是比较简单的。主要就是通过前面查找出的main_class, 然后通过在jvm的实例中获取到的各方法的函数指针,然后按照字节码的规范,调用 MainClass.main(String[]) 方法。当然了,为了兼容其他非main的场景,它还有很多附加处理逻辑。
另外,如何翻译java代码,并执行,这是jvm的重中之重。即编译原理的拿手好戏,也是我们普通程序员越不过的坎!但至少,我们来到了门槛前面!
2. jvm启动框架
前面讲了加载main方法的过程,大致理解了c如何启动调用java的main的。那么,这又是如何调用准备的呢,在这之前都需要做哪些准备呢?
实际上,这是平台相关的实现。
// share/bin/java.c/** Entry point.*/intJLI_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.)*/// 解析命令行参数,选择一jre版本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();}// 加载VM, 重中之重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);}// macos/bin/java_md_macos.c// MacOSX we may continue in the same threadintJVMInit(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;// 调用 JavaMain()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);}}
以上是mac调用 javaMain的方式。在windows, 以及linux上则稍有不同。
// windows/bin/java_md.cintJVMInit(InvocationFunctions* ifn, jlong threadStackSize,int argc, char **argv,int mode, char *what, int ret){ShowSplashScreen();return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);}// java.cintContinueInNewThread(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;// 传入 JavaMain() 函数参数,在新线程中运行 JavaMainrslt = 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;}}/** Block current thread and continue execution in a new thread*/intContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {int rslt = 0;unsigned thread_id;#ifndef STACK_SIZE_PARAM_IS_A_RESERVATION#define STACK_SIZE_PARAM_IS_A_RESERVATION (0x10000)#endif/** STACK_SIZE_PARAM_IS_A_RESERVATION is what we want, but it's not* supported on older version of Windows. Try first with the flag; and* if that fails try again without the flag. See MSDN document or HotSpot* source (os_win32.cpp) for details.* 调用底层内核方法,创建新线程,将JavaMain(args)作为函数调用处理。创建后立即执行* 如果第一次带参创建失败,则弃参重试一次*/HANDLE thread_handle =(HANDLE)_beginthreadex(NULL,(unsigned)stack_size,continuation,args,STACK_SIZE_PARAM_IS_A_RESERVATION,&thread_id);if (thread_handle == NULL) {thread_handle =(HANDLE)_beginthreadex(NULL,(unsigned)stack_size,continuation,args,0,&thread_id);}/* AWT preloading (AFTER main thread start) */#ifdef ENABLE_AWT_PRELOAD/* D3D preloading */if (awtPreloadD3D != 0) {char *envValue;/* D3D routines checks env.var J2D_D3D if no appropriate* command line params was specified*/envValue = getenv("J2D_D3D");if (envValue != NULL && JLI_StrCaseCmp(envValue, "false") == 0) {awtPreloadD3D = 0;}/* Test that AWT preloading isn't disabled by J2D_D3D_PRELOAD env.var */envValue = getenv("J2D_D3D_PRELOAD");if (envValue != NULL && JLI_StrCaseCmp(envValue, "false") == 0) {awtPreloadD3D = 0;}if (awtPreloadD3D < 0) {/* If awtPreloadD3D is still undefined (-1), test* if it is turned on by J2D_D3D_PRELOAD env.var.* By default it's turned OFF.*/awtPreloadD3D = 0;if (envValue != NULL && JLI_StrCaseCmp(envValue, "true") == 0) {awtPreloadD3D = 1;}}}if (awtPreloadD3D) {AWTPreload(D3D_PRELOAD_FUNC);}#endif /* ENABLE_AWT_PRELOAD */if (thread_handle) {// 等待线程结束,并获取返回码WaitForSingleObject(thread_handle, INFINITE);GetExitCodeThread(thread_handle, &rslt);CloseHandle(thread_handle);} else {// 如果实在是创建线程失败,则自身直接执行该方法即可rslt = continuation(args);}#ifdef ENABLE_AWT_PRELOADif (awtPreloaded) {AWTPreloadStop();}#endif /* ENABLE_AWT_PRELOAD */return rslt;}
在linux中的创建线程如下:
// solaris/bin/java_md_solinux.c/** Block current thread and continue execution in a new thread*/intContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {int rslt;#ifdef __linux__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);}// 常见的 pthread_xx 方式 创建线程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);#else /* ! __linux__ */thread_t tid;long flags = 0;if (thr_create(NULL, stack_size, (void *(*)(void *))continuation, args, flags, &tid) == 0) {void * tmp;thr_join(tid, NULL, &tmp);rslt = (int)tmp;} else {/* See above. Continue in current thread if thr_create() failed */rslt = continuation(args);}#endif /* __linux__ */return rslt;}
即相同的语义,不同平台下的各自实现而已。
通过C语言的函数传递,从而进行调用。通过创建一个新线程,调用JavaMain(); 由javaMain处理所有java事务。而自身则只需等待JavaMain完成即可。这也符合面向过程编程思想。
3. jvm初始化小结
我们平时是通过 java xxx.xxx 或者 java -jar xx.jar 启动jvm, 通过这两篇文章,我们也看清了其背后的原理。就是通过解析各种参数,验证各种参数,验证jre环境,然后验证jre是否可用,最后将指定的mainClass加载出来。丢到一个新的线程中去执行,将执行权力转发给java代码,最后等待该线程完成。
应该说事个流程没有难点,或者说一切都很理所当然。但其中的核心,都是通过加载JavaVM()去做的,也就是真正的jvm. 然后通过或者对应的几个接口方法地址,进行调用,从而完成启动任务。所以,java虽是一个启动命令,但核心并不在这里。而是在 hotspot 或其他地方。jdk 毕竟只是一个工具箱而已。jre 才是关键。

腾讯、阿里、滴滴后台面试题汇总总结 — (含答案)
面试:史上最全多线程面试题 !
最新阿里内推Java后端面试题
JVM难学?那是因为你没认真看完这篇文章

关注作者微信公众号 —《JAVA烂猪皮》
了解更多java后端架构知识以及最新面试宝典


看完本文记得给作者点赞+在看哦~~~大家的支持,是作者源源不断出文的动力
作者:等你归去来
出处:https://www.cnblogs.com/yougewe/p/14400859.html
