您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)java命令中本質(zhì)邏輯的示例分析,小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
在日常編碼中,有了ide的支持,我們已經(jīng)很少直接在命令行中直接執(zhí)行java XXX命令去啟動(dòng)一個(gè)項(xiàng)目了。然而我們有沒有想過,一個(gè)簡(jiǎn)單的java命令背后究竟做了些什么事情?讓我們看下下面幾個(gè)簡(jiǎn)單的問題
1.java命令之后可以跟很多參數(shù),那么這些參數(shù)是如何被解析的?為何-version會(huì)返回版本號(hào)而如果緊跟一個(gè)類名則會(huì)啟動(dòng)jvm?
2.為何我們自己定義的入口方法必須滿足如下的簽名?是否還有其他可能性?
public static void main(String[] args) { }
3.如果我們需要調(diào)用自己寫的native方法,必須顯式地通過 System.loadLibrary() 加載動(dòng)態(tài)鏈接庫。而如果我們查看java的基礎(chǔ)類(Thread、Object、Class等,這些類中有非常多的native方法),則會(huì)發(fā)現(xiàn)其內(nèi)部并沒有調(diào)用 System.loadLibrary() 方法,而是由靜態(tài)構(gòu)造函數(shù)中的 registerNatives() 負(fù)責(zé)注冊(cè)其它的natvie方法。
例如:Thread.java
class Thread implements Runnable { private static native void registerNatives(); static { registerNatives(); } ... }
不過 registerNatives() 本身也是一個(gè)native方法,那它所在動(dòng)態(tài)鏈接庫又是何時(shí)被加載的?
問題1和問題2自不必多言,答案一定在java命令中
而對(duì)于問題3,因?yàn)門hread、Object、Class等等作為jdk的原生類,其相關(guān)的動(dòng)態(tài)鏈接庫就是jvm本身(windows系統(tǒng)是 jvm.dll ,linux 系統(tǒng)是libjvm.so,mac 系統(tǒng)是 libjvm.dylib),所以很容易推測(cè)其加載動(dòng)態(tài)鏈接庫的過程一定是在jvm的啟動(dòng)流程中。
今天我們就以上面3個(gè)問題為引子,探究一下java命令背后的本質(zhì),即jvm的啟動(dòng)流程
既然需要分析jvm的啟動(dòng)流程,那么jdk和hotspot的源碼是不可少的。下載地址:http://hg.openjdk.java.net/jdk8
主入口方法
查看 java.c,jdk 目錄 /src/java.base/share/native/libjli,該目錄會(huì)因?yàn)椴煌姹镜膉dk有不同
入口方法是 JLI_Launch ,當(dāng)然其中內(nèi)容很多,我們挑選其中的重點(diǎn)部分來看
int JLI_Launch(args) { ... //創(chuàng)建執(zhí)行環(huán)境 CreateExecutionEnvironment(&argc, &argv, jrepath, sizeof(jrepath), jvmpath, sizeof(jvmpath), jvmcfg, sizeof(jvmcfg)); ... //加載jvm if (!LoadJavaVM(jvmpath, &ifn)) { return(6); } ... //解析命令行參數(shù),例如-h,-version等等 if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath)) { return(ret); } ... //啟動(dòng)jvm return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret); }
那么接下去就分別查看這幾個(gè)主要方法的邏輯
這個(gè)方法根據(jù)操作系統(tǒng)的不同有不同的邏輯,下面以linux系統(tǒng)為例
查看 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的版本,這里是根據(jù)jre的路徑,找到j(luò)vm.cfg文件 */ if (ReadKnownVMs(jvmcfg, JNI_FALSE) < 1) { JLI_ReportErrorMessage(CFG_ERROR7); exit(1); } jvmpath[0] = '\0'; /** * 檢查jvm的版本,如果命令行中有指定,那么會(huì)采用指定的jvm版本,否則使用默認(rèn)的 */ jvmtype = CheckJvmType(pargc, pargv, JNI_FALSE); if (JLI_StrCmp(jvmtype, "ERROR") == 0) { JLI_ReportErrorMessage(CFG_ERROR9); exit(4); } /** * 獲取動(dòng)態(tài)鏈接庫的路徑 */ if (!GetJVMPath(jrepath, jvmtype, jvmpath, so_jvmpath, 0 )) { JLI_ReportErrorMessage(CFG_ERROR8, jvmtype, jvmpath); exit(4); } }
主要有以下幾4個(gè)步驟
1.確定jre的路徑
這里會(huì)優(yōu)先尋找應(yīng)用程序當(dāng)前目錄
if (GetApplicationHome(path, pathsize)) { ... } if (GetApplicationHomeFromDll(path, pathsize)) { ... }
2.根據(jù)jre拼接 jvm.cfg 的路徑,并讀取可用的jvm配置
一般 jvm.cfg 文件在 /jre/lib 中,其內(nèi)容如下:
-server KNOWN -client IGNORE
上述2行配置分別對(duì)應(yīng)不同的jvm的版本,例如第一行 -server KNOWN ,那么在加載jvm動(dòng)態(tài)鏈接庫的時(shí)候就會(huì)去 /jre/lib/server 目錄中尋找
3.檢查jvm類型
在執(zhí)行java命令的時(shí)候,可以通過命令指定jvm版本,如果沒有指定,那么就采用jvm.cfg中的第一個(gè)jvm版本
i = KnownVMIndex(arg); if (i >= 0) { ... } else if (JLI_StrCCmp(arg, "-XXaltjvm=") == 0 || JLI_StrCCmp(arg, "-J-XXaltjvm=") == 0) { ... }
4.獲取動(dòng)態(tài)鏈接庫的路徑
根據(jù)前面檢查jvm類型的結(jié)果,獲取到對(duì)應(yīng)的jvm動(dòng)態(tài)鏈接庫的路徑,全部按照默認(rèn)的話,在Mac系統(tǒng)中獲取到的lib路徑如下
路徑中的server正是之前在cfg文件中讀取到的-server
/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/jre/lib/server/libjvm.dylib
查看 java_md_solinux.c,jdk 目錄 /src/java.base/unix/native/libjli
jboolean LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn) { /** * 加載動(dòng)態(tài)鏈接庫,這里調(diào)用的是dlopen,而不是普通的open */ libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL); ... /** * 將jvm中的"JNI_CreateJavaVM"方法鏈接到j(luò)dk的CreateJavaVM方法上 */ ifn->CreateJavaVM = (CreateJavaVM_t) dlsym(libjvm, "JNI_CreateJavaVM"); /** * 調(diào)用CreateJavaVM方法 */ if (ifn->CreateJavaVM == NULL) { JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror()); return JNI_FALSE; } /** * 將jvm中的"JNI_GetDefaultJavaVMInitArgs"方法鏈接到j(luò)dk的GetDefaultJavaVMInitArgs方法上 */ ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t) dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs"); /** * 調(diào)用GetDefaultJavaVMInitArgs方法 */ if (ifn->GetDefaultJavaVMInitArgs == NULL) { JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror()); return JNI_FALSE; } /** * 將jvm中的"JNI_GetCreatedJavaVMs"方法鏈接到j(luò)dk的GetCreatedJavaVMs方法上 */ ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t) dlsym(libjvm, "JNI_GetCreatedJavaVMs"); /** * 調(diào)用GetCreatedJavaVMs方法 */ if (ifn->GetCreatedJavaVMs == NULL) { JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror()); return JNI_FALSE; } }
主要步驟如下:
1.加載動(dòng)態(tài)鏈接庫,也正是我們第一個(gè)問題的答案所在
dlopen方法是dynamic link open的縮寫,在打開文件的同時(shí),加載動(dòng)態(tài)鏈接庫??梢酝ㄟ^ man dlopen 命令查看說明
man dlopen dlopen -- load and link a dynamic library or bundle
2.鏈接并調(diào)用jvm中的 JNI_CreateJavaVM 、GetDefaultJavaVMInitArgs、GetCreatedJavaVMs
dlsym方法是dynamic link symbol的縮寫,將動(dòng)態(tài)鏈接庫中的方法鏈接到當(dāng)前方法上
man dlsym dlsym -- get address of a symbol
這3個(gè)方法顧名思義,分別是創(chuàng)建jvm、獲取默認(rèn)的jvm啟動(dòng)參數(shù)、獲取創(chuàng)建完成的jvm。這3個(gè)方法的入口在
hotspot 目錄 /src/share/vm/prims/jni.cpp
文件中,有興趣的同學(xué)可以自行查看
查看 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); } ... }
其中的參數(shù)一共有2大類。
1.類似于 --version 的參數(shù)在解析之后會(huì)直接返回
2.類似于 -mx、-mx 的參數(shù)則會(huì)通過 AddOption 方法添加成為 VM option
/* * Adds a new VM option with the given name and value. */ void AddOption(char *str, void *info) { ... }
JVMInit:?jiǎn)?dòng)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) { //在一個(gè)新線程中啟動(dòng)jvm return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret); }
在該方法中,會(huì)調(diào)用 ContinueInNewThread 創(chuàng)建一個(gè)新線程啟動(dòng)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) { ... /** * 創(chuàng)建一個(gè)新的線程創(chuàng)建jvm并調(diào)用main方法 */ rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args); return (ret != 0) ? ret : rslt; }
在該方法中,會(huì)調(diào)用 ContinueInNewThread0 并傳入 JavaMain 入口方法
查看 java_md_solinux.c,jdk 目錄 /src/java.base/unix/native/libjli
/** * 阻塞當(dāng)前線程,并在一個(gè)新線程中執(zhí)行main方法 */ int ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) { //創(chuàng)建一個(gè)新線程執(zhí)行傳入的continuation,其實(shí)也就是外面?zhèn)魅氲膍ain方法 if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) { void * tmp; //當(dāng)前線程阻塞 pthread_join(tid, &tmp); rslt = (int)(intptr_t)tmp; } ... }
在該方法中,會(huì)創(chuàng)建一個(gè)新線程調(diào)用傳入的 main 方法,而當(dāng)前線程則阻塞
因?yàn)檫@里pthread_join是等待在運(yùn)行main方法的線程上,所以java程序運(yùn)行時(shí),如果main線程運(yùn)行結(jié)束了,整個(gè)進(jìn)程就會(huì)結(jié)束,而由main啟動(dòng)的子線程對(duì)整個(gè)進(jìn)程是沒有影響的
查看 java.c,jdk 目錄 /src/java.base/share/native/libjli
int JNICALL JavaMain(void * _args) { //啟動(dòng)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回調(diào)java代碼中的main方法 (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); }
這里對(duì)于main方法的方法名和簽名都是固定判斷的,所以無論是什么java程序,入口方法必須是 public static void main(String[] args)
到此jvm從準(zhǔn)備啟動(dòng)到最后執(zhí)行main方法的代碼流程就結(jié)束了。因?yàn)檫@個(gè)流程的方法分散在不同的文件中,會(huì)很讓人頭暈,所以我總結(jié)了成了以下結(jié)構(gòu),方便大家理解
入口方法:JLI_Launch |--------->創(chuàng)建執(zhí)行環(huán)境:CreateExecutionEnvironment | |--------->獲取jre的路徑:GetJREPath | |--------->讀取jvm配置:ReadKnownVMs | |--------->檢查jvm類型:CheckJvmType | |--------->獲取jvm動(dòng)態(tài)鏈接庫路徑:GetJVMPath |--------->加載jvm動(dòng)態(tài)鏈接庫:LoadJavaVM | |--------->加載動(dòng)態(tài)鏈接庫:dlopen | |--------->鏈接jvm方法:dlsym |--------->解析命令行參數(shù):ParseArguments | |--------->類似于 --version 的參數(shù)在解析之后會(huì)直接返回 | |--------->類似于 -mx、-mx 的參數(shù)則會(huì)通過 AddOption 方法添加成為 VM option |--------->啟動(dòng)jvm并執(zhí)行main方法:JVMInit |--------->創(chuàng)建一個(gè)新線程并執(zhí)行后續(xù)任務(wù):ContinueInNewThread |--------->創(chuàng)建新線程執(zhí)行main方法:ContinueInNewThread0(JavaMain) |--------->創(chuàng)建新線程,用于執(zhí)行傳入的main方法:pthread_create |--------->阻塞當(dāng)前線程:pthread_join |--------->獲取main方法:JavaMain |--------->加載主類:LoadMainClass |--------->根據(jù)簽名獲取main方法的id:GetStaticMethodID |--------->執(zhí)行main方法:CallStaticVoidMethod
關(guān)于“java命令中本質(zhì)邏輯的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,使各位可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),請(qǐng)把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。