1. 前言
上文介紹了HotSpot編譯和調試的方法,而這篇文章將邁出正式調試的第一步——調試HotSpot的啟動過程。
學習啟動過程可以幫助我們了解程序的入口,並對虛擬機的運行有個整體的把握,方便日后深入學習具體的一些模塊。
2. 整體感知啟動過程
整體的感知啟動過程可以在啟動時添加_JAVA_LAUNCHER_DEBUG=1
的環境變量。這樣JVM會輸出詳細的打印。
通過這些打印,我們大致能了解到啟動過程發生了什么。
----_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----
從上面的打印大致可以看出有這么幾步:
- 打印了啟動器的狀態,包括版本號、程序名等
- 打印了傳給程序命令行參數,第一個是java命令的相信路徑,第二個虛擬機將要執行的java代碼
- 解析JRE路徑,解析jvm.cfg
- 加載libjvm庫
- 解析虛擬機參數
- 初始化虛擬機
- 虛擬機加載要執行的Java主類,解析參數並執行
3. 啟動過程說明
我們就以上面划分的階段為整體脈絡,再深入的看看各階段的具體邏輯。
3.1 啟動入口
虛擬機程序運行的入口是在main.c/main方法中。之后會調用java.c/JLI_Launch方法。
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);
}
這個方法比較長,但是可以划分為幾個部分去分析:
3.1.1 前期初始化工作和狀態打印
這里的初始化部分包括一些參數值的聲明,特殊結構體InvocationFuntions
的聲明,啟動器的初始化。
其中聲明的參數會在后續的啟動過程用來存儲相關信息,例如保存JVM、JRE相關路徑等。
InvocationFuntions
是個重要的結構體,其中包含了創建JVM會被調用的三個函數指針。
typedef struct {
CreateJavaVM_t CreateJavaVM; //指向負責創建JavaVM和JNIEnv結構的函數指針
GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs; //指向獲取默認JVM初始參數的函數指針
GetCreatedJavaVMs_t GetCreatedJavaVMs; //指向獲取JVM的函數指針
} InvocationFunctions;
InitLaucher
方法主要就是根據_JAVA_LAUNCHER_DEBUG
這個環境變量會決定后續是否輸出DEBUG的打印。
在開啟了launcher_debug后,DumpState()
方法會打印出啟動狀態,並且之后打印出命令行參數。
3.1.2 檢驗版本
SelectVersion
會驗證用戶指定的java版本和實際執行的java版本是否兼容,如果不兼容會退出進程。用戶可以通過_JAVA_VERSION_SET
的環境變量或是jar包中manifest文件等方式指定運行的java版本。
3.1.3 創建執行環境
CreateExecutionEnvironment
會為后續的啟動創建執行環境,這一步驟中主要是確定jdk所在的路徑,解析jvmcfg和確認libjvm是否存在等。
- 主要是根據處理器類型和主路徑確定出JRE的路徑
- 以同樣的方式確定jvm.cfg的文件位置,並解析jvm.cfg(jvm.cfg里面是一些虛擬機的默認配置,如常見的指定以客戶端或服務端模式運行)
- 檢查虛擬機類型(-server/-client),可以是jvm.cfg指定或是由啟動參數指定
- 確定libjvm庫的位置,校驗庫是否存在,這個庫核心的函數庫
3.1.4 設置虛擬機環境
SetJvmEnviroment
主要解析NativeMemoryTracking參數,可以用來追蹤本地內存的使用情況
3.1.5 加載虛擬機
前期環境准備好之后,LoadJavaVM()
會從之前確定的路徑,加載libjvm庫,並將其中的庫中JNI_CreateJavaVM
,JNI_GetDefaultJavaVMInitArgs
和JNI_GetCreatedJavaVMs
三個函數賦值給ifn。
這三個函數會在之后創建虛擬機時被使用。
3.1.6 解析參數
這里有兩個部分,一是解析命令行傳入的參數,看是否有特定的JVM配置選項。這些參數會被用於后續虛擬機的創建上。這一過程主要發生在ParseArguments()
中。另一個部分就是添加一些特定的虛擬機參數,發生在SetJavaCommandLineProp
、SetJavaLaucherProp
和SetJavaLaucherPlatformProps
中。
3.1.7 虛擬機初始化
在環境都准備好之后,會由JVMInit()
執行虛擬機初始化工作,首先會通過ShowSplashScreen()
方法加載啟動動畫,之后會進入CountinueInNewThread()
方法,由新的線程負責創建虛擬機的工作。
3.2 在新線程中繼續虛擬機的創建
通過上文的介紹,我們找到了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()
函數。也就是說新創建的線程將會開始執行該函數。
3.3 虛擬機創建、Java程序運行的主過程——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
}
3.3.1 參數解析
之前上文解析得到的命令行參數等都被封裝在JavaMainArgs
結構體中,傳給了JavaMain
方法。因此需要從這個結構體中取回參數。
另外,還創建了一些變量用於之后的過程中存儲值,譬如jclass,jmethodID等。
3.3.2 初始化虛擬機,打印相關信息
上述代碼中的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()
方法創建虛擬機 - 給兩個重要的指針賦值,分別是JavaVM * 和 JNIEnv
- 一些后置處理,例如通過JVMTI(可以說是虛擬機的工具接口,提供了對虛擬機調試、監測等等的功能)、事件提交等
針對第一點,Threads::create_vm()
是負責創建虛擬機,整個過程相對復雜,需要初始化很多模塊,創建虛擬機的后台線程,加載必要的類等等,這里不做深入分析。之后有時間可以單獨分析這一過程。
針對第二點中提到的兩個數據結構,非常重要。我們可以看看它們的具體的內容。
JavaVM
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
JNIEnv
結構內部包的是JNINativeInterface結構,這個結構同樣定義了很多函數指針,代碼太長,這里就不直接貼出了。有興趣的可以在jni.h
中自行查看。如果對結構中的方法分類的話,可以分成以下幾類:
- 獲取虛擬機信息
- 獲取相關類和方法,方法執行
- 獲取/設置對象字段
- 靜態方法、靜態變量的獲取與設置
- 常見類型的對象的創建和釋放
- 創建直接內存、訪問鎖等
總之,提供了通過C++代碼訪問Java程序的能力(這對於從事JNI開發的人來說十分重要)。
3.3.3 確定Java程序的主類
了解完成虛擬機的初始化過程后,再回到JavaMain()方法中,之后是通過LoadMainClass()
或GetApplicationClass()
方法確定Java代碼的主類。
如果我們在運行指定了Java類,那么這個類就是主類。這里還會調用LauncherHelper.checkAndLoadMain()
檢驗主類是否合法。LauncherHelper
的Java代碼,這里就是上面介紹的JNIEnv的能力在C++的代碼中執行Java代碼。
對於一些沒有主類的程序,需要通過LaucherHelper.getApplicationClass()
確定程序類。
3.3.4 從主類中獲取main方法的methodID,並調用方法
再確定了mainClass之后,還需要找到該類定義的main()
,獲取main()方法,然后將程序參數封裝,傳遞給main()
執行,線程會以此為入口,開始執行Java程序。
這里的找方法和執行方法同樣是依賴了JNIEnv中GetStaticMethodID
和CallStaticVoidMethod
。
所以我們的main()方法總是static void
的。
3.3.5 獲取執行結果,退出虛擬機
當線程從Main()方法中返回,說明Java程序已經執行完成(或是異常退出),這時候虛擬機會檢查運行結果,並解綁線程銷毀虛擬機,最終退出。