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 函數(shù)類// 通過引入 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的調(diào)用實(shí)例,在之前的步驟中,通過加載相應(yīng)動態(tài)鏈接方法,保存起來的/*** 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);}// 調(diào)用 LEAVE() 方法的目的在于主動銷毀jvm線程// 且退出當(dāng)前方法調(diào)用,即 LEAVE() 后方法不再被執(zhí)行/** 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();}// 釋放內(nèi)存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() 方法前執(zhí)行初始化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 */// 構(gòu)建args[] 參數(shù)mainArgs = CreateApplicationArgs(env, argv, argc);CHECK_EXCEPTION_NULL_LEAVE(mainArgs);/* Invoke main method. */// 調(diào)用java實(shí)現(xiàn)的main()方法// XX:: 重要實(shí)現(xiàn)(*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()調(diào)用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, 主要是調(diào)用 CreateJavaVM() 方法,進(jìn)行創(chuàng)建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的實(shí)現(xiàn),要加載main類,還是比較簡單的。主要就是通過前面查找出的main_class, 然后通過在jvm的實(shí)例中獲取到的各方法的函數(shù)指針,然后按照字節(jié)碼的規(guī)范,調(diào)用 MainClass.main(String[]) 方法。當(dāng)然了,為了兼容其他非main的場景,它還有很多附加處理邏輯。
另外,如何翻譯java代碼,并執(zhí)行,這是jvm的重中之重。即編譯原理的拿手好戲,也是我們普通程序員越不過的坎!但至少,我們來到了門檻前面!
2. jvm啟動框架
前面講了加載main方法的過程,大致理解了c如何啟動調(diào)用java的main的。那么,這又是如何調(diào)用準(zhǔn)備的呢,在這之前都需要做哪些準(zhǔn)備呢?
實(shí)際上,這是平臺相關(guān)的實(shí)現(xiàn)。
// 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);// 打印狀態(tài)DumpState();// 跟蹤調(diào)用啟動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.)*/// 解析命令行參數(shù),選擇一jre版本SelectVersion(argc, argv, &main_class);CreateExecutionEnvironment(&argc, &argv,jrepath, sizeof(jrepath),jvmpath, sizeof(jvmpath),jvmcfg, sizeof(jvmcfg));if (!IsJavaArgs()) {// 設(shè)置一些特殊的環(huán)境變量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;// 解析更多參數(shù)信息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.*/// 解析參數(shù)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;// 調(diào)用 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調(diào)用 javaMain的方式。在windows, 以及l(fā)inux上則稍有不同。
// 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() 函數(shù)參數(shù),在新線程中運(yùn)行 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.* 調(diào)用底層內(nèi)核方法,創(chuàng)建新線程,將JavaMain(args)作為函數(shù)調(diào)用處理。創(chuàng)建后立即執(zhí)行* 如果第一次帶參創(chuàng)建失敗,則棄參重試一次*/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) {// 等待線程結(jié)束,并獲取返回碼WaitForSingleObject(thread_handle, INFINITE);GetExitCodeThread(thread_handle, &rslt);CloseHandle(thread_handle);} else {// 如果實(shí)在是創(chuàng)建線程失敗,則自身直接執(zhí)行該方法即可rslt = continuation(args);}#ifdef ENABLE_AWT_PRELOADif (awtPreloaded) {AWTPreloadStop();}#endif /* ENABLE_AWT_PRELOAD */return rslt;}
在linux中的創(chuàng)建線程如下:
// 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 方式 創(chuàng)建線程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;}
即相同的語義,不同平臺下的各自實(shí)現(xiàn)而已。
通過C語言的函數(shù)傳遞,從而進(jìn)行調(diào)用。通過創(chuàng)建一個新線程,調(diào)用JavaMain(); 由javaMain處理所有java事務(wù)。而自身則只需等待JavaMain完成即可。這也符合面向過程編程思想。
3. jvm初始化小結(jié)
我們平時是通過 java xxx.xxx ?或者 java -jar xx.jar 啟動jvm, 通過這兩篇文章,我們也看清了其背后的原理。就是通過解析各種參數(shù),驗證各種參數(shù),驗證jre環(huán)境,然后驗證jre是否可用,最后將指定的mainClass加載出來。丟到一個新的線程中去執(zhí)行,將執(zhí)行權(quán)力轉(zhuǎn)發(fā)給java代碼,最后等待該線程完成。
應(yīng)該說事個流程沒有難點(diǎn),或者說一切都很理所當(dāng)然。但其中的核心,都是通過加載JavaVM()去做的,也就是真正的jvm. 然后通過或者對應(yīng)的幾個接口方法地址,進(jìn)行調(diào)用,從而完成啟動任務(wù)。所以,java雖是一個啟動命令,但核心并不在這里。而是在 hotspot 或其他地方。jdk 畢竟只是一個工具箱而已。jre 才是關(guān)鍵。

騰訊、阿里、滴滴后臺面試題匯總總結(jié) — (含答案)
面試:史上最全多線程面試題 !
最新阿里內(nèi)推Java后端面試題
JVM難學(xué)?那是因為你沒認(rèn)真看完這篇文章

關(guān)注作者微信公眾號 —《JAVA爛豬皮》
了解更多java后端架構(gòu)知識以及最新面試寶典


看完本文記得給作者點(diǎn)贊+在看哦~~~大家的支持,是作者源源不斷出文的動力
作者:等你歸去來
出處:https://www.cnblogs.com/yougewe/p/14400859.html
