在上一章《在docker上編譯openjdk8》裏,咱們在docker容器內成功編譯了openjdk8的源碼,有沒有讀者朋友產生過這個念頭:「能不能修改openjdk源碼,構建一個不同凡響的jdk「,今天咱們就來閱讀一些openjdk的源碼,再嘗試作些小改動並驗證。java
咱們先編譯openjdk:首先經過命令git clone git@github.com:zq2599/centos7buildopenjdk8.git下載構建鏡像所需的文件,下載後打開控制檯進入centos7buildopenjdk8目錄,執行linux
docker build -t bolingcavalryopenjdk:0.0.1 .複製代碼
這樣就構建好了鏡像文件,再執行啓動docker容器的命令(命令中的參數「–security-opt seccomp=unconfined」有特殊用處,稍後會講到):git
docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1複製代碼
而後執行如下命令進入容器的控制檯:github
docker exec -it jdk001 /bin/bash複製代碼
進入容器的控制檯後執行如下兩個命令開始編譯:docker
./configure --with-debug-level=slowdebug
make all ZIP_DEBUGINFO_FILES=0 DISABLE_HOTSPOT_OS_VERSION_CHECK=OK CONF=linux-x86_64-normal-server-slowdebug複製代碼
以上就是編譯openjdk的步驟了,請你們開始編譯吧,由於等會兒會用到,咱們要用編譯好的jdk作調試。vim
如今開始看源碼吧,本次分析的目標是針對咱們熟悉的java -version命令,當咱們在終端敲下這個命令的時候,jvm到底作了些什麼呢?windows
整個分析驗證的流程是這樣的:centos
準備工做:在容器內經過vim看源碼是很不方便的,因此我這裏是在電腦上覆制了一份openjdk的源碼(下載地址:http://www.java.net/download/openjdk/jdk8/promoted/b132/openjdk-8-src-b132-03mar2014.zip),用sublime text3打開openjdk源碼,真正到了要修改的時候再去docker容器裏經過vi修改。安全
尋找程序入口bash
第一步就是把程序的入口和源碼對應起來,先要找到入口main函數,步驟以下:
gdb --args ./java -version複製代碼
效果以下圖,能夠看到已進入GDB命令行模式,能夠繼續輸入GDB命令了:
輸入b main命令,在main函數打斷點,此時GDB會返回斷點位置的信息,以下圖,main函數的位置在/usr/local/openjdk/jdk/src/share/bin/main.c, line 97:
再輸入l命令能夠打印源碼,以下圖:
在容器外的電腦上,經過sublime text3或者其餘ide打開main.c,以下圖,開始讀代碼吧:
順序閱讀代碼
main函數中的代碼並很少,但有幾個宏定義會擾亂咱們思路,從字面上看#ifdef _WIN32這樣的宏應該是windows平臺下才會生效的,但總不能每次都靠字面推斷,此時打斷點單步執行是最直接的方法,可是在打斷點以前,咱們先解決前面遺留的一個問題吧,此問題挺重要的:
還記得咱們啓動docker容器的命令麼:
docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1複製代碼
命令中的–security-opt seccomp=unconfined參數有什麼用?爲什麼要留在打斷點以前再次提到這個參數?
這個參數和Docker的安全機制有關,具體的文檔連接在這裏,請讀者們自行參悟,本人的英文太差就不獻醜了,簡單的說就是Docker有個Seccomp filtering功能,以伯克萊封包過濾器(Berkeley Packet Filter,縮寫BPF)的方式容許用戶對容器內的系統調用(syscall)作自定義的「allow」, 「deny」, 「trap」, 「kill」, or 「trace」操做,因爲Seccomp filtering的限制,在默認的配置下,會致使咱們在用GDB的時候run失敗,因此在執行docker run的時候加入–security-opt seccomp=unconfined這個參數,能夠關閉seccomp profile的功能;
我以前不知道seccomp profile的限制,用命令docker run –name=jdk001 -idt bolingcavalryopenjdk:0.0.1啓動了容器,編譯能夠成功,可是在用GDB調試的時候出了問題,以下圖:
上圖中,黃框中的「進入GDB」和「b main」(添加斷點)兩個命令都能正常執行,可是紅框中的」r」(運行程序)命令在執行的時候提示錯誤「Error disabling address space randomization: Operation not permitted」,在執行」n」(單步執行)命令的時候提示程序不在運行中。
遺留問題已經澄清,能夠繼續跟蹤代碼了,以前咱們已經在GDB輸入了」b mian」,給main函數打了斷點,如今輸入」r」開始執行,而後就會看到main函數的斷點已經生效,輸入」n」能夠跟蹤代碼執行到了哪一行,以下圖:
原來代碼執行的位置分別是97,122,123,125這四行,和下圖的源碼徹底對應上了:
有了GDB神器,能夠愉快的閱讀源碼了:
main.c的main函數中,調用JLILaunch函數,在Sublime text3中,將鼠標放置在」JLILaunch」位置,會彈出一個小窗口,上面是JLI_Launch函數的聲明和定義的兩個連接,以下圖:
點擊第一個連接,跳轉到JLI_Launch函數的定義位置:
//根據環境變量初始化debug標誌位,後續的日誌是否會打印靠這個debug標誌控制了
InitLauncher(javaw);
//若是設置了debug,就會打印一些輔助信息
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);
} //若是設置debug標誌位,就打印命令行參數,並加入額外參數
//選擇jre版本,在jar包的manifest文件或者命令行中均可以對jre版本進行設置
SelectVersion(argc, argv, &main_class);
/*
設置一些參數,例如jvmpath的值被設置成jdk所在目錄下的「lib/amd64/server/l」子目錄,再加上宏定義JVM_DLL的值"libjvm.so",即:/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so
*/
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
jvmcfg, sizeof(jvmcfg));
//記錄加載libjvm.so的起始時間,在加載結束後能夠獲得並打印出加載libjvm.so的耗時
ifn.CreateJavaVM = 0;
ifn.GetDefaultJavaVMInitArgs = 0;
if (JLI_IsTraceLauncher()) {
start = CounterGet();
}
//加載/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/libjvm.so
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;
if (IsJavaArgs()) {
/* Preprocess wrapper arguments */
TranslateApplicationArgs(jargc, jargv, &argc, &argv);
if (!AddApplicationOptions(appclassc, appclassv)) {
return(1);
}
} else {
//classpath處理
/* Set default CLASSPATH */
cpath = getenv("CLASSPATH");
if (cpath == NULL) {
cpath = ".";
}
SetClassPath(cpath);
}
//解析命令行的參數
if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
{
return(ret);
}複製代碼
到這裏先不要繼續往下讀,咱們進ParseArguments函數中去看看:
如上圖紅框所示,解析到」-version」參數的時候,會將printVersion變量設置爲JNI_TRUE並當即返回。
繼續閱讀JLI_Launch函數:
//若是有-jar參數,就會根據參數設置classpath
if (mode == LM_JAR) {
SetClassPath(what);
}
//添加一個用於HotSpot虛擬機的參數"-Dsun.java.command"
SetJavaCommandLineProp(what, argc, argv);
/* Set the -Dsun.java.launcher pseudo property */
//添加一個參數-Dsun.java.launcher=SUN_STANDARD,這樣JVM就知道是他的建立者的身份
SetJavaLauncherProp();
//獲取當前進程ID,放入參數-Dsun.java.launcher.pid中,這樣JVM就知道是他的建立者的進程ID
SetJavaLauncherPlatformProps();
return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);複製代碼
接下來在JVMInit函數中,ContinueInNewThread函數中會調用ContinueInNewThread0函數,而且把JavaMain函數作爲入參傳遞給ContinueInNewThread0,ContinueInNewThread0的代碼以下:
//若是指定了線程棧的大小,就在此設置到線程屬性變量attr中
if (stack_size > 0) {
pthread_attr_setstacksize(&attr, stack_size);
}
//建立線程,外部傳入的JavaMain也在此傳給子線程,子線程建立成功後,會先執行JavaMain(也就是continuation參數)
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..
*/
//若建立子線程失敗,在當前線程直接執行外面傳入的JavaMain函數
rslt = continuation(args);
}
//再也不使用線程屬性,將其銷燬
pthread_attr_destroy(&attr);複製代碼
在閱讀ContinueInNewThread0函數源碼的時候碰見了下圖紅框中的註釋,這是我見過的最優秀的註釋(僅表明我的看法),當我看到pthreadcreate被調用時就在想「建立線程失敗會怎樣?」,而後這個註釋出現了,告訴我「若是由於某些緣由(例如內存溢出)致使建立線程失敗,當前線程還會繼續執行JavaMain,可是在後續的操做中依然有可能發生錯誤,例如JNICreateJavaVM函數會建立一些新的線程,所以,在當前線程執行JavaMain只是作一次嘗試」。
在恰當的位置將問題說清楚,並對後續發展作適當的提示,好的代碼加上好的註釋真是讓人受益不淺。
接着上面的分析,在新的線程中JavaMain函數會被調用,這個函數內容以下:
//windows和linux下,RegisterThread是個空函數,mac有實現
RegisterThread();
//記錄當前時間,統計JVM初始化耗時的時候用到
start = CounterGet();
//調用libjvm.so庫中的CreateJavaVM方法初始化虛擬機
if (!InitializeJVM(&vm, &env, &ifn)) {
JLI_ReportErrorMessage(JVM_ERROR1);
exit(1);
}
//調用java類的靜態方法(sun.launcher.LauncherHelper.showSettings),打印jvm的設置信息
if (showSettings != NULL) {
ShowSettings(env, showSettings);
CHECK_EXCEPTION_LEAVE(1);
}
/*
調用java類的靜態方法(sun.misc.Version.print),打印:
1.java版本信息
2.java運行時版本信息
3.java虛擬機版本信息
*/
if (printVersion || showVersion) {
PrintJavaVersion(env, showVersion);
CHECK_EXCEPTION_LEAVE(0);
if (printVersion) {
LEAVE();
}
}複製代碼
讀到這裏能夠不用讀後面的代碼了,由於printVersion變量爲true,因此在執行完PrintJavaVersion後,會調用LEAVE()函數使虛擬機與當前線程分離,而後就是線程結束,進程結束。
此時,咱們應該聚焦PrintJavaVersion函數,來看看平時執行」java -version」的內容是怎麼產生的。
進入PrintJavaVersion函數,內容並很少,但能學到c語言的jvm是如何執行java類中的靜態方法的,以下:
static void
PrintJavaVersion(JNIEnv *env, jboolean extraLF)
{
jclass ver;
jmethodID print;
//從bootStrapClassLoader中查找sun.misc.Version
NULL_CHECK(ver = FindBootStrapClass(env, "sun/misc/Version"));
/*
因爲命令行參數中沒有-showVersion參數,因此extraLF不等於JNI_TRUE,因此此處調用的是sun.misc.Version.print方法,若是命令是"java -showVersion",那麼調用的就是pringlin方法了
*/
NULL_CHECK(print = (*env)->GetStaticMethodID(env,
ver,
(extraLF == JNI_TRUE) ? "println" : "print",
"()V"
)
);
(*env)->CallStaticVoidMethod(env, ver, print);
}複製代碼
讀到這裏,本次閱讀源碼的工做彷佛要結束了,但事情沒那麼簡單,讀者們請在openjdk文件夾下搜索Version.java文件,雖然能搜到幾個Version.java,但是包路徑符合sun/misc/Version.java的文件只有一個,而這個Version.java的上層目錄是test目錄,不是src目錄,顯然只是測試代碼,並非上面的PrintJavaVersion函數中調用的Version類:
如今問題來了,真正的Version類到底在哪呢?
剛纔搜索Version.java文件的時候,咱們搜的是下載openjdk源碼解壓以後的文件夾,如今咱們回到docker容器中的/usr/local/openjdk目錄下,輸入find ./ -name Version.java試試,結果以下圖,在build目錄下,發現了四個sun/misc/Version.java文件:
在上圖中,sun/misc/Version.java文件一共有四個,後三個Version.java文件的路徑中帶有getprofile1,getprofile2這類的路徑,此處猜想是在某些場景或者設置的前提下才會產生(實在對不起各位讀者,這是個人猜想,具體緣由至今還麼搞清楚,有知道的請告訴一些,謝謝啦),因此這裏咱們仍是聚焦第一個文件吧:
/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/gensrc/sun/misc/Version.java
Version.java這個文件,在下載的源碼中沒有,而編譯成功後的build目錄下卻有,而且文件的路徑中有gensrc這個目錄,顯然是在編譯過程當中產生的,好吧,咱們從Makefile中去尋找答案去:在Makefile文件中,會調用Main.gmk,以下圖:
Main.gmk中會調用BuildJdk.gmk,以下圖:
BuildJdk.gmk中會調用GenerateSources.gmk,以下圖:
GenerateSources.gmk中會調用GensrcMisc.gmk,以下圖:
打開GensrcMisc.gmk文件後,一切都一目瞭然了,以下圖中的代碼所示,以/src/share/classes/sun/misc/Version.java.template文件做爲模板,經過sed命令將Version.java.template文件中的一些佔位符替換成已有的變量,替換了佔位符以後的文件就是Version.java
咱們能夠看到一共有五個佔位符被替換:
@@launcher_name@@ 替換成 $(LAUNCHER_NAME)
@@java_version@@ 替換成 $(RELEASE)
@@java_runtime_version@@ 替換成 $(FULL_VERSION)
@@java_runtime_name@@ 替換成 $(RUNTIME_NAME)
@@java_profile_name@@ 替換成 $(call profile_version_name, $@)複製代碼
先看看Version.java.template中是什麼:
果真有五個佔位符,而後有個靜態方法public static void init(),裏面把佔位符對應的內容設置到全局屬性中去了。
終於搞清楚了,原來Version.java源自Version.java.template文件,在編譯構建的時候被生成,生成的時候Version.java.template文件中的佔位符被替換成對應的變量。
如今,在docker容器裏,執行命令vi /usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/gensrc/sun/misc,打開Version.java看看吧,以下圖:
果真所有被替換了,再配合static代碼塊中的init方法,也就意味着這個類被加載的時候,應用就有了這三個全局的屬性:java.version,java.runtime.version,java.runtime.name
搞清楚了Version.java的前因後果,還剩一個小問題要搞清楚,在GensrcMisc.gmk文件中,用sed命令替換Version.java.template文件中的佔位符的時候,那些用來替換佔位符的變量是哪裏來的呢?或者說Version.java文件中javaversion =」1.8.0-internal-debug」,javaruntimename =」OpenJDK Runtime Environment」,javaruntimeversion = 「1.8.0-internal-debug-201704210439-b00」這些表達式中的和」1.8.0-internal-debug」,「OpenJDK Runtime Environment」」,「1.8.0-internal-debug-2017042104_39-b00」究竟來自何處?這時候最簡單的辦法就是用」RELEASE」,」FULLVERSION」,」RUNTIMENAME」去作全局搜索,很快就能查出來,我這來梳理一下吧:
openjdk/configure文件中調用common/autoconf/configurecommon/autoconf/configure中調用autogen.shautogen.sh中有以下操做:
把configure.ac中的內容作替換後輸出到generated-configure.sh,其中用到了autoconfig作配置
configure.ac中調用basics.m4basics.m4中調用spec.gmk.inspec.gmk.in中明確寫出了JDKVERSION,RUNTIMENAME這些變量的定義,以下圖:
PRODUCTNAME和PRODUCTSUFFIX是autoconfig的配置項,在openjdk/common/autoconf/version-numbers文件中定義,這是個autoconfig的配置文件,以下圖:
變量的來源梳理完畢,接着看代碼吧,sun.misc.Version類的print方法,以下圖,一如既往的簡答明瞭,將一些全局屬性取出而後打印出來:
至此,java -version命令對應的源碼分析完畢,簡答的總結一下,就是入口的main函數中,經過調用java的Version類的print靜態方法,將一些變量打印出來,這些變量是經過autoconfig輸出到自動生成的java源碼中的;
既然已經讀懂了源碼,如今該親自動手實踐一下啦,這裏咱們作兩個改動,記得是在docker容器中用vi工具去改:
修改Version.java.template文件,讓java -version在執行的時候多輸出一行代碼,以下圖紅框位置:
修改/usr/local/openjdk/common/autoconf/version-numbers,修改PRODUCTSUFFIX的值,根據以前的理解,PRODUCTSUFFIX修改後,輸出的runtime name會有變化,改動以下:
改動完畢,回到/usr/local/openjdk目錄下,執行下面兩行命令,開始編譯:
./configure --with-debug-level=slowdebug
make all ZIP_DEBUGINFO_FILES=0 DISABLE_HOTSPOT_OS_VERSION_CHECK=OK CONF=linux-x86_64-normal-server-slowdebug複製代碼
編譯結束後,去/usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/bin目錄執行./java -version,獲得的輸出以下圖,能夠看到咱們的改動已經生效了
至次,本次閱讀,修改,調試和編譯openjdk8的實踐就結束了,其實JavaMain函數作了不少事情,此次只是看到其中打印信息的那一部分而已,後面的加載class,執行java類等都尚未看到,有興趣的讀者能夠先對java的類加載作個初步瞭解,再繼續閱讀JavaMain函數,相信您會有更多收穫的。