在上一章《在docker上編譯openjdk8》裏,咱們在docker容器內成功編譯了openjdk8的源碼,有沒有讀者朋友產生過這個念頭:「能不能修改openjdk源碼,構建一個不同凡響的jdk「,今天咱們就來閱讀一些openjdk的源碼,再嘗試作些小改動並驗證。java
咱們先編譯openjdk: 首先經過命令git clone git@github.com:zq2599/centos7_build_openjdk8.git下載構建鏡像所需的文件,下載後打開控制檯進入centos7_build_openjdk8目錄,執行linux
docker build -t bolingcavalryopenjdk:0.0.1 .
這樣就構建好了鏡像文件,再執行啓動docker容器的命令(<font color="red">命令中的參數「–security-opt seccomp=unconfined」有特殊用處,稍後會講到</font>):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作調試。shell
如今開始看源碼吧,本次分析的目標是針對咱們熟悉的java -version命令,當咱們在終端敲下這個命令的時候,jvm到底作了些什麼呢?vim
整個分析驗證的流程是這樣的:windows
準備工做: 在容器內經過vim看源碼是很不方便的,因此我這裏是在電腦上覆制了一份openjdk的源碼(下載地址:http://www.java.net/download/openjdk/jdk8/promoted/b132/openjdk-8-src-b132-03_mar_2014.zip ),用sublime text3打開openjdk源碼,真正到了要修改的時候再去docker容器裏經過vi修改。centos
尋找程序入口安全
第一步就是把程序的入口和源碼對應起來,先要找到入口main函數,步驟以下:
gdb --args ./java -version
效果以下圖,能夠看到已進入GDB命令行模式,能夠繼續輸入GDB命令了:
輸入b main命令,在main函數打斷點,此時GDB會返回斷點位置的信息,以下圖,main函數的位置在<font color="red">/usr/local/openjdk/jdk/src/share/bin/main.c, line 97</font>:
再輸入l命令能夠打印源碼,以下圖:
在容器外的電腦上,經過sublime text3或者其餘ide打開main.c,以下圖,開始讀代碼吧:
順序閱讀代碼
main函數中的代碼並很少,但有幾個宏定義會擾亂咱們思路,從字面上看#ifdef _WIN32這樣的宏應該是windows平臺下才會生效的,但總不能每次都靠字面推斷,此時打斷點單步執行是最直接的方法,可是在打斷點以前,咱們先解決前面遺留的一個問題吧,<font color="red">此問題挺重要的</font>:
還記得咱們啓動docker容器的命令麼:
docker run --name=jdk001 --security-opt seccomp=unconfined -idt bolingcavalryopenjdk:0.0.1
命令中的<font color="blue">–security-opt seccomp=unconfined</font>參數有什麼用?爲什麼要留在打斷點以前再次提到這個參數?
這個參數和Docker的安全機制有關,具體的文檔連接在這裏,請讀者們自行參悟,本人的英文太差就不獻醜了,簡單的說就是Docker有個Seccomp filtering功能,以伯克萊封包過濾器(Berkeley Packet Filter,縮寫BPF)的方式容許用戶對容器內的系統調用(syscall)作自定義的「allow」, 「deny」, 「trap」, 「kill」, or 「trace」操做,因爲Seccomp filtering的限制,在默認的配置下,會致使咱們在用GDB的時候run失敗,因此在執行docker run的時候加入<font color="red">–security-opt seccomp=unconfined</font>這個參數,能夠關閉seccomp profile的功能;
我以前不知道seccomp profile的限制,用命令<font color="blue">docker run –name=jdk001 -idt bolingcavalryopenjdk:0.0.1</font>啓動了容器,編譯能夠成功,可是在用GDB調試的時候出了問題,以下圖:
上圖中,黃框中的「進入GDB」和「b main」(添加斷點)兩個命令都能正常執行,可是紅框中的」r」(運行程序)命令在執行的時候提示錯誤<font color="red">「Error disabling address space randomization: Operation not permitted」</font>,在執行」n」(單步執行)命令的時候提示程序不在運行中。
遺留問題已經澄清,能夠繼續跟蹤代碼了,以前咱們已經在GDB輸入了」b mian」,給main函數打了斷點,如今輸入」r」開始執行,而後就會看到main函數的斷點已經生效,輸入」n」能夠跟蹤代碼執行到了哪一行,以下圖:
原來代碼執行的位置分別是97,122,123,125這四行,和下圖的源碼徹底對應上了:
有了GDB神器,能夠愉快的閱讀源碼了:
main.c的main函數中,調用JLI_Launch函數,在Sublime text3中,將鼠標放置在」JLI_Launch」位置,會彈出一個小窗口,上面是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函數,<font color="red">而且把JavaMain函數作爲入參傳遞給ContinueInNewThread0,</font>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函數源碼的時候碰見了下圖紅框中的註釋,這是我見過的最優秀的註釋(僅表明我的看法),當我看到pthread_create被調用時就在想「建立線程失敗會怎樣?」,而後這個註釋出現了,告訴我「若是由於某些緣由(例如內存溢出)致使建立線程失敗,當前線程還會繼續執行JavaMain,可是在後續的操做中依然有可能發生錯誤,例如JNI_CreateJavaVM函數會建立一些新的線程,所以,在當前線程執行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); }
讀到這裏,本次閱讀源碼的工做彷佛要結束了,<font color="red">但事情沒那麼簡單</font>,讀者們請在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文件的路徑中帶有get_profile_1,get_profile_2這類的路徑,此處猜想是在某些場景或者設置的前提下才會產生(實在對不起各位讀者,這是個人猜想,具體緣由至今還麼搞清楚,有知道的請告訴一些,謝謝啦),因此這裏咱們仍是聚焦第一個文件吧:
/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文件後,一切都一目瞭然了,以下圖中的代碼所示,以<font color="blue">/src/share/classes/sun/misc/Version.java.template</font>文件做爲模板,經過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容器裏,執行命令<font color="blue">vi /usr/local/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/gensrc/sun/misc</font> ,打開Version.java看看吧,以下圖:
果真所有被替換了,再配合static代碼塊中的init方法,也就意味着這個類被加載的時候,應用就有了這三個全局的屬性:java.version,java.runtime.version,java.runtime.name
搞清楚了Version.java的前因後果,還剩一個小問題要搞清楚,在GensrcMisc.gmk文件中,用sed命令替換Version.java.template文件中的佔位符的時候,那些用來替換佔位符的變量是哪裏來的呢?或者說Version.java文件中java_version =」1.8.0-internal-debug」,java_runtime_name =」OpenJDK Runtime Environment」,java_runtime_version = 「1.8.0-internal-debug-_2017_04_21_04_39-b00」這些表達式中的和」1.8.0-internal-debug」,「OpenJDK Runtime Environment」」,「1.8.0-internal-debug-_2017_04_21_04_39-b00」究竟來自何處? 這時候最簡單的辦法就是用」RELEASE」,」FULL_VERSION」,」RUNTIME_NAME」去作全局搜索,很快就能查出來,我這來梳理一下吧:
openjdk/configure文件中調用common/autoconf/configure common/autoconf/configure中調用autogen.sh autogen.sh中有以下操做:
把configure.ac中的內容作替換後輸出到generated-configure.sh,其中用到了autoconfig作配置
configure.ac中調用basics.m4 basics.m4中調用spec.gmk.in spec.gmk.in中明確寫出了JDK_VERSION,RUNTIME_NAME這些變量的定義,以下圖:
PRODUCT_NAME和PRODUCT_SUFFIX是autoconfig的配置項,在openjdk/common/autoconf/version-numbers文件中定義,這是個autoconfig的配置文件,以下圖:
變量的來源梳理完畢,接着看代碼吧,sun.misc.Version類的print方法,以下圖,一如既往的簡答明瞭,將一些全局屬性取出而後打印出來:
至此,java -version命令對應的源碼分析完畢,簡答的總結一下,就是入口的main函數中,經過調用java的Version類的print靜態方法,將一些變量打印出來,這些變量是經過autoconfig輸出到自動生成的java源碼中的;
既然已經讀懂了源碼,如今該親自動手實踐一下啦,這裏咱們作兩個改動,<font color="red">記得是在docker容器中用vi工具去改</font>:
修改Version.java.template文件,讓java -version在執行的時候多輸出一行代碼,以下圖紅框位置:
修改/usr/local/openjdk/common/autoconf/version-numbers,修改PRODUCT_SUFFIX的值,根據以前的理解,PRODUCT_SUFFIX修改後,輸出的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函數,相信您會有更多收穫的。