前言
在日常編碼中,有了ide的支援,我們已經很少直接在命令列中直接執行java XXX命令去啟動一個專案了。然而我們有沒有想過,一個簡單的java命令背後究竟做了些什麼事情?讓我們看下下面幾個簡單的問題
1.java命令之後可以跟很多引數,那麼這些引數是如何被解析的?為何-version會返回版本號而如果緊跟一個類名則會啟動jvm?
2.為何我們自己定義的入口方法必須滿足如下的簽名?是否還有其他可能性?
public static void main(String[] args) {
}
3.如果我們需要呼叫自己寫的native方法,必須顯式地通過 System.loadLibrary() 載入動態連結庫。而如果我們檢視java的基礎類(Thread、Object、Class等,這些類中有非常多的native方法),則會發現其內部並沒有呼叫 System.loadLibrary() 方法,而是由靜態建構函式中的 registerNatives() 負責註冊其它的natvie方法。
例如:Thread.java
class Thread implements Runnable {
private static native void registerNatives();
static {
registerNatives();
}
...
}
不過 registerNatives() 本身也是一個native方法,那它所在動態連結庫又是何時被載入的?
問題1和問題2自不必多言,答案一定在java命令中
而對於問題3,因為Thread、Object、Class等等作為jdk的原生類,其相關的動態連結庫就是jvm本身(windows系統是 jvm.dll ,linux 系統是libjvm.so,mac 系統是 libjvm.dylib),所以很容易推測其載入動態連結庫的過程一定是在jvm的啟動流程中。
今天我們就以上面3個問題為引子,探究一下java命令背後的本質,即jvm的啟動流程
jvm的啟動流程分析
既然需要分析jvm的啟動流程,那麼jdk和hotspot的原始碼是不可少的。下載地址:http://hg.openjdk.java.net/jdk8
主入口方法
檢視 java.c,jdk 目錄 /src/java.base/share/native/libjli,該目錄會因為不同版本的jdk有不同
入口方法是 JLI_Launch ,當然其中內容很多,我們挑選其中的重點部分來看
int
JLI_Launch(args)
{
...
//建立執行環境
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
jvmcfg, sizeof(jvmcfg));
...
//載入jvm
if (!LoadJavaVM(jvmpath, &ifn)) {
return(6);
}
...
//解析命令列引數,例如-h,-version等等
if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
{
return(ret);
}
...
//啟動jvm
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
}
那麼接下去就分別檢視這幾個主要方法的邏輯
CreateExecutionEnvironment:建立執行環境
這個方法根據作業系統的不同有不同的邏輯,下面以linux系統為例
檢視 java_md_solinux.c,jdk 目錄 /src/java.base/unix/native/libjli
CreateExecutionEnvironment(args) {
/**
* 獲取jre的路徑
*/
if (!GetJREPath(jrepath, so_jrepath, JNI_FALSE) ) {
JLI_ReportErrorMessage(JRE_ERROR1);
exit(2);
}
JLI_Snprintf(jvmcfg, so_jvmcfg, "%s%slib%s%sjvm.cfg",
jrepath, FILESEP, FILESEP, FILESEP);
/**
* 讀取jvm的版本,這裡是根據jre的路徑,找到jvm.cfg檔案
*/
if (ReadKnownVMs(jvmcfg, JNI_FALSE) < 1) {
JLI_ReportErrorMessage(CFG_ERROR7);
exit(1);
}
jvmpath[0] = '\0';
/**
* 檢查jvm的版本,如果命令列中有指定,那麼會採用指定的jvm版本,否則使用預設的
*/
jvmtype = CheckJvmType(pargc, pargv, JNI_FALSE);
if (JLI_StrCmp(jvmtype, "ERROR") == 0) {
JLI_ReportErrorMessage(CFG_ERROR9);
exit(4);
}
/**
* 獲取動態連結庫的路徑
*/
if (!GetJVMPath(jrepath, jvmtype, jvmpath, so_jvmpath, 0 )) {
JLI_ReportErrorMessage(CFG_ERROR8, jvmtype, jvmpath);
exit(4);
}
}
主要有以下幾4個步驟
1.確定jre的路徑
這裡會優先尋找應用程式當前目錄
if (GetApplicationHome(path, pathsize)) {
...
}
if (GetApplicationHomeFromDll(path, pathsize)) {
...
}
2.根據jre拼接 jvm.cfg 的路徑,並讀取可用的jvm配置
一般 jvm.cfg 檔案在 /jre/lib 中,其內容如下:
-server KNOWN
-client IGNORE
上述2行配置分別對應不同的jvm的版本,例如第一行 -server KNOWN ,那麼在載入jvm動態連結庫的時候就會去 /jre/lib/server 目錄中尋找
3.檢查jvm型別
在執行java命令的時候,可以通過命令指定jvm版本,如果沒有指定,那麼就採用jvm.cfg中的第一個jvm版本
i = KnownVMIndex(arg);
if (i >= 0) {
...
}
else if (JLI_StrCCmp(arg, "-XXaltjvm=") == 0 || JLI_StrCCmp(arg, "-J-XXaltjvm=") == 0) {
...
}
4.獲取動態連結庫的路徑
根據前面檢查jvm型別的結果,獲取到對應的jvm動態連結庫的路徑,全部按照預設的話,在Mac系統中獲取到的lib路徑如下
路徑中的server正是之前在cfg檔案中讀取到的-server
/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/server/libjvm.dylib
LoadJavaVM:載入jvm
檢視 java_md_solinux.c,jdk 目錄 /src/java.base/unix/native/libjli
jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
/**
* 載入動態連結庫,這裡呼叫的是dlopen,而不是普通的open
*/
libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);
...
/**
* 將jvm中的"JNI_CreateJavaVM"方法連結到jdk的CreateJavaVM方法上
*/
ifn->CreateJavaVM = (CreateJavaVM_t)
dlsym(libjvm, "JNI_CreateJavaVM");
/**
* 呼叫CreateJavaVM方法
*/
if (ifn->CreateJavaVM == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
/**
* 將jvm中的"JNI_GetDefaultJavaVMInitArgs"方法連結到jdk的GetDefaultJavaVMInitArgs方法上
*/
ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");
/**
* 呼叫GetDefaultJavaVMInitArgs方法
*/
if (ifn->GetDefaultJavaVMInitArgs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
/**
* 將jvm中的"JNI_GetCreatedJavaVMs"方法連結到jdk的GetCreatedJavaVMs方法上
*/
ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
dlsym(libjvm, "JNI_GetCreatedJavaVMs");
/**
* 呼叫GetCreatedJavaVMs方法
*/
if (ifn->GetCreatedJavaVMs == NULL) {
JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());
return JNI_FALSE;
}
}
主要步驟如下:
1.載入動態連結庫,也正是我們第一個問題的答案所在
dlopen方法是dynamic link open的縮寫,在開啟檔案的同時,載入動態連結庫。可以通過 man dlopen 命令檢視說明
man dlopen
dlopen -- load and link a dynamic library or bundle
2.連結並呼叫jvm中的 JNI_CreateJavaVM 、GetDefaultJavaVMInitArgs、GetCreatedJavaVMs
dlsym方法是dynamic link symbol的縮寫,將動態連結庫中的方法連結到當前方法上
man dlsym
dlsym -- get address of a symbol
這3個方法顧名思義,分別是建立jvm、獲取預設的jvm啟動引數、獲取建立完成的jvm。這3個方法的入口在
hotspot 目錄 /src/share/vm/prims/jni.cpp
檔案中,有興趣的同學可以自行檢視
ParseArguments:解析命令列引數
檢視 java.c,jdk 目錄 /src/java.base/share/native/libjli
static jboolean
ParseArguments(int *pargc, char ***pargv,
int *pmode, char **pwhat,
int *pret, const char *jrepath)
{
...
if (JLI_StrCmp(arg, "--version") == 0) {
printVersion = JNI_TRUE;
printTo = USE_STDOUT;
return JNI_TRUE;
}
...
if (JLI_StrCCmp(arg, "-ss") == 0 ||
JLI_StrCCmp(arg, "-oss") == 0 ||
JLI_StrCCmp(arg, "-ms") == 0 ||
JLI_StrCCmp(arg, "-mx") == 0) {
char *tmp = JLI_MemAlloc(JLI_StrLen(arg) + 6);
sprintf(tmp, "-X%s", arg + 1); /* skip '-' */
AddOption(tmp, NULL);
}
...
}
其中的引數一共有2大類。
1.類似於 --version 的引數在解析之後會直接返回
2.類似於 -mx、-mx 的引數則會通過 AddOption 方法新增成為 VM option
/*
* Adds a new VM option with the given name and value.
*/
void
AddOption(char *str, void *info)
{
...
}
JVMInit:啟動jvm
檢視 java_md_solinux.c,jdk 目錄 /src/java.base/unix/native/libjli
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
//在一個新執行緒中啟動jvm
return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}
在該方法中,會呼叫 ContinueInNewThread 建立一個新執行緒啟動jvm
檢視 java.c,jdk 目錄 /src/java.base/share/native/libjli
int
ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
int argc, char **argv,
int mode, char *what, int ret)
{
...
/**
* 建立一個新的執行緒建立jvm並呼叫main方法
*/
rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
return (ret != 0) ? ret : rslt;
}
在該方法中,會呼叫 ContinueInNewThread0 並傳入 JavaMain 入口方法
檢視 java_md_solinux.c,jdk 目錄 /src/java.base/unix/native/libjli
/**
* 阻塞當前執行緒,並在一個新執行緒中執行main方法
*/
int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
//建立一個新執行緒執行傳入的continuation,其實也就是外面傳入的main方法
if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
void * tmp;
//當前執行緒阻塞
pthread_join(tid, &tmp);
rslt = (int)(intptr_t)tmp;
}
...
}
在該方法中,會建立一個新執行緒呼叫傳入的 main 方法,而當前執行緒則阻塞
因為這裡pthread_join是等待在執行main方法的執行緒上,所以java程式執行時,如果main執行緒執行結束了,整個程式就會結束,而由main啟動的子執行緒對整個程式是沒有影響的
檢視 java.c,jdk 目錄 /src/java.base/share/native/libjli
int JNICALL
JavaMain(void * _args)
{
//啟動jvm
if (!InitializeJVM(&vm, &env, &ifn)) {
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
...
//載入主類
mainClass = LoadMainClass(env, mode, what);
//找到main方法id
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
//通過jni回撥java程式碼中的main方法
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
}
這裡對於main方法的方法名和簽名都是固定判斷的,所以無論是什麼java程式,入口方法必須是 public static void main(String[] args)
到此jvm從準備啟動到最後執行main方法的程式碼流程就結束了。因為這個流程的方法分散在不同的檔案中,會很讓人頭暈,所以我總結了成了以下結構,方便大家理解
入口方法:JLI_Launch
|--------->建立執行環境:CreateExecutionEnvironment
| |--------->獲取jre的路徑:GetJREPath
| |--------->讀取jvm配置:ReadKnownVMs
| |--------->檢查jvm型別:CheckJvmType
| |--------->獲取jvm動態連結庫路徑:GetJVMPath
|--------->載入jvm動態連結庫:LoadJavaVM
| |--------->載入動態連結庫:dlopen
| |--------->連結jvm方法:dlsym
|--------->解析命令列引數:ParseArguments
| |--------->類似於 --version 的引數在解析之後會直接返回
| |--------->類似於 -mx、-mx 的引數則會通過 AddOption 方法新增成為 VM option
|--------->啟動jvm並執行main方法:JVMInit
|--------->建立一個新執行緒並執行後續任務:ContinueInNewThread
|--------->建立新執行緒執行main方法:ContinueInNewThread0(JavaMain)
|--------->建立新執行緒,用於執行傳入的main方法:pthread_create
|--------->阻塞當前執行緒:pthread_join
|--------->獲取main方法:JavaMain
|--------->載入主類:LoadMainClass
|--------->根據簽名獲取main方法的id:GetStaticMethodID
|--------->執行main方法:CallStaticVoidMethod