上文介紹了HotSpot編譯和調試的方法,而這篇文章將邁出正式調試的第一步——調試HotSpot的啓動過程。
學習啓動過程能夠幫助咱們瞭解程序的入口,並對虛擬機的運行有個總體的把握,方便往後深刻學習具體的一些模塊。java
總體的感知啓動過程能夠在啓動時添加_JAVA_LAUNCHER_DEBUG=1
的環境變量。這樣JVM會輸出詳細的打印。
經過這些打印,咱們大體能瞭解到啓動過程發生了什麼。linux
----_JAVA_LAUNCHER_DEBUG---- Launcher state: debug:on javargs:off program name:java launcher name:openjdk javaw:off fullversion:1.8.0-internal-debug-xieshang_2020_12_18_09_49-b00 dotversion:1.8 ergo_policy:DEFAULT_ERGONOMICS_POLICY Command line args: argv[0] = /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin/java argv[1] = com.insanexs/HelloHotspot JRE path is /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk jvm.cfg[0] = ->-server<- jvm.cfg[1] = ->-client<- 1 micro seconds to parse jvm.cfg Default VM: server Does `/home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so' exist ... yes. mustsetenv: FALSE JVM path is /home/xieshang/learn-jvm/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so 1 micro seconds to LoadJavaVM JavaVM args: version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 5 option[ 0] = '-Dsun.java.launcher.diag=true' option[ 1] = '-Djava.class.path=/home/xieshang/learn-open-jdk' option[ 2] = '-Dsun.java.command=com.insanexs/HelloHotspot' option[ 3] = '-Dsun.java.launcher=SUN_STANDARD' option[ 4] = '-Dsun.java.launcher.pid=4485' 1 micro seconds to InitializeJVM Main class is 'com.insanexs/HelloHotspot' App's argc is 0 1 micro seconds to load main class ----_JAVA_LAUNCHER_DEBUG----
從上面的打印大體能夠看出有這麼幾步:windows
咱們就以上面劃分的階段爲總體脈絡,再深刻的看看各階段的具體邏輯。數據結構
虛擬機程序運行的入口是在main.c/main方法中。以後會調用java.c/JLI_Launch方法。app
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));//解析相關環境 獲取jre路徑、jvmlib庫和jvm.cfg /************************** 設置虛擬機環境 ********************/ if (!IsJavaArgs()) { SetJvmEnvironment(argc,argv); } ifn.CreateJavaVM = 0; ifn.GetDefaultJavaVMInitArgs = 0; if (JLI_IsTraceLauncher()) { start = CounterGet(); } /************************** 加載虛擬機 ********************/ if (!LoadJavaVM(jvmpath, &ifn)) { //加載 主要是從jvmlib庫中解析函數地址 賦值給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"); //添加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) { //若是是java -jar 則覆蓋classpath 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); }
這個方法比較長,可是能夠劃分爲幾個部分去分析:jvm
這裏的初始化部分包括一些參數值的聲明,特殊結構體InvocationFuntions
的聲明,啓動器的初始化。
其中聲明的參數會在後續的啓動過程用來存儲相關信息,例如保存JVM、JRE相關路徑等。
InvocationFuntions
是個重要的結構體,其中包含了建立JVM會被調用的三個函數指針。ide
typedef struct { CreateJavaVM_t CreateJavaVM; //指向負責建立JavaVM和JNIEnv結構的函數指針 GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs; //指向獲取默認JVM初始參數的函數指針 GetCreatedJavaVMs_t GetCreatedJavaVMs; //指向獲取JVM的函數指針 } InvocationFunctions;
InitLaucher
方法主要就是根據_JAVA_LAUNCHER_DEBUG
這個環境變量會決定後續是否輸出DEBUG的打印。
在開啓了launcher_debug後,DumpState()
方法會打印出啓動狀態,而且以後打印出命令行參數。函數
SelectVersion
會驗證用戶指定的java版本和實際執行的java版本是否兼容,若是不兼容會退出進程。用戶能夠經過_JAVA_VERSION_SET
的環境變量或是jar包中manifest文件等方式指定運行的java版本。工具
CreateExecutionEnvironment
會爲後續的啓動建立執行環境,這一步驟中主要是肯定jdk所在的路徑,解析jvmcfg和確認libjvm是否存在等。post
SetJvmEnviroment
主要解析NativeMemoryTracking參數,能夠用來追蹤本地內存的使用狀況
前期環境準備好以後,LoadJavaVM()
會從以前肯定的路徑,加載libjvm庫,並將其中的庫中JNI_CreateJavaVM
,JNI_GetDefaultJavaVMInitArgs
和JNI_GetCreatedJavaVMs
三個函數賦值給ifn。
這三個函數會在以後建立虛擬機時被使用。
這裏有兩個部分,一是解析命令行傳入的參數,看是否有特定的JVM配置選項。這些參數會被用於後續虛擬機的建立上。這一過程主要發生在ParseArguments()
中。另外一個部分就是添加一些特定的虛擬機參數,發生在SetJavaCommandLineProp
、SetJavaLaucherProp
和SetJavaLaucherPlatformProps
中。
在環境都準備好以後,會由JVMInit()
執行虛擬機初始化工做,首先會經過ShowSplashScreen()
方法加載啓動動畫,以後會進入CountinueInNewThread()
方法,由新的線程負責建立虛擬機的工做。
經過上文的介紹,咱們找到了java.c/ConutinueInNewThread()
的方法。這個方法分爲兩個部分,第一部分就是肯定線程棧的深度,第二部分就是由ContinueInNewThread0()
這個方法實現真正的虛擬機建立過程。
int ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) { int rslt; #ifndef __solaris__ 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); } //建立線程並執行方法 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 /* __solaris__ */ 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 /* !__solaris__ */ return rslt; }
在這個方法中,首先調用了pthread_create()
函數建立了一個新線程,同時舊線程被jion等待新線程運行完成後返回。
pthread_create()
是unix操做系統建立線程的函數,它的第一個參數表示線程標識,第二參數表示線程屬性,第三個參數表示建立線程所要執行函數的地址,第四個參數則是將要執行的函數的參數。
等到新線程運行完成後,舊的線程也會返回。此時說明運行結束,進程將會退出。
須要注意的是此時傳入的函數地址,它是指向java.c/JavaMain()
函數。也就是說新建立的線程將會開始執行該函數。
新建立的線程會去執行JavaMain()
函數,正式進入了建立虛擬機、運行Java代碼的過程。
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(); /*******************初始化JVM、打印相關信息********************************/ 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 */ //打印Java程序的參數 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]); } } /******************獲取Java程序的主類***************************/ 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);//加載mainClass 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); //獲取application class 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); // JVM 初始化後置處理 /* * 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方法************************/ mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); //獲取main class的 main(String[] args)方法 CHECK_EXCEPTION_NULL_LEAVE(mainID); /*******************封裝參數,調用main方法*****************/ /* Build platform specific argument array */ mainArgs = CreateApplicationArgs(env, argv, argc); //封裝 main(String[] args) 方法的參數args CHECK_EXCEPTION_NULL_LEAVE(mainArgs); /* Invoke main method. */ (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); //調用main(String args)方法 /* * 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(); //線程解綁 銷燬JVM }
以前上文解析獲得的命令行參數等都被封裝在JavaMainArgs
結構體中,傳給了JavaMain
方法。所以須要從這個結構體中取回參數。
另外,還建立了一些變量用於以後的過程當中存儲值,譬如jclass,jmethodID等。
上述代碼中的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); //經過ifn的函數指針 調用CreateJavaVM函數初始化JavaVM 和 JNIEnv JLI_MemFree(options); return r == JNI_OK; }
先獲取虛擬機參數,在經過ifn結構體中CreateJavaVM指針,調用正式建立Java虛擬機的函數JNI_CreateJavaVM
。
JNI_CreateJavaVM
代碼的主要流程以下:
Threads::create_vm()
方法建立虛擬機針對第一點,Threads::create_vm()
是負責建立虛擬機,整個過程相對複雜,須要初始化不少模塊,建立虛擬機的後臺線程,加載必要的類等等,這裏不作深刻分析。以後有時間能夠單獨分析這一過程。
針對第二點中提到的兩個數據結構,很是重要。咱們能夠看看它們的具體的內容。
JavaVM結構內部包的是JNIInvokeInterface_
結構,所以咱們直接看一下JNIInvokeInterface_
的結構
struct JNIInvokeInterface_ { //預留字段 void *reserved0; void *reserved1; void *reserved2; jint (JNICALL *DestroyJavaVM)(JavaVM *vm); //銷燬虛擬機的函數指針 jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args); //綁定線程的函數指針 jint (JNICALL *DetachCurrentThread)(JavaVM *vm); //解綁線程的函數指針 jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version); //獲取JNIEnv結構的函數指針 jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args);//將線程轉爲後臺線程 };
能夠看到主要是一些和虛擬機操做的相關函數。
JNIEnv
結構內部包的是JNINativeInterface結構,這個結構一樣定義了不少函數指針,代碼太長,這裏就不直接貼出了。有興趣的能夠在jni.h
中自行查看。若是對結構中的方法分類的話,能夠分紅如下幾類:
總之,提供了經過C++代碼訪問Java程序的能力(這對於從事JNI開發的人來講十分重要)。
瞭解完成虛擬機的初始化過程後,再回到JavaMain()方法中,以後是經過LoadMainClass()
或GetApplicationClass()
方法肯定Java代碼的主類。
若是咱們在運行指定了Java類,那麼這個類就是主類。這裏還會調用LauncherHelper.checkAndLoadMain()
檢驗主類是否合法。LauncherHelper
的Java代碼,這裏就是上面介紹的JNIEnv的能力在C++的代碼中執行Java代碼。
對於一些沒有主類的程序,須要經過LaucherHelper.getApplicationClass()
肯定程序類。
再肯定了mainClass以後,還須要找到該類定義的main()
,獲取main()方法,而後將程序參數封裝,傳遞給main()
執行,線程會以此爲入口,開始執行Java程序。
這裏的找方法和執行方法一樣是依賴了JNIEnv中GetStaticMethodID
和CallStaticVoidMethod
。
因此咱們的main()方法老是static void
的。
當線程從Main()方法中返回,說明Java程序已經執行完成(或是異常退出),這時候虛擬機會檢查運行結果,並解綁線程銷燬虛擬機,最終退出。