Java 虛拟主機裝載過程
簡述
衆所周知java.exe是java class檔案的執行程式,但實際上java.exe程式隻是一個執行的外殼,它會裝載jvm.dll(windows下,下皆以windows平台為例,linux下和solaris下其實類似,為:libjvm.so),這個動态連接配接庫才是java虛拟機的實際操作處理所在。文探究java.exe程式是如何查找和裝載jvm.dll動态庫,并調用它進行class檔案執行處理的。
源代碼
本文分析之代碼,《JavaTM 2 SDK, Standard Edition, v1.4.2 fcsCommunity Source Release》,可從sun官方網站下載下傳,主分析的源代碼為:
j2se/src/share/bin/java.c
j2se/src/windows/bin/java_md.c
‘java程式’源代碼
所謂‘java程式’,包括jdk中的java.exe/javac.exe/javadoc.exe,java.c源代碼中通過JAVA_ARGS宏來控制生成的代碼,如果該宏沒定義則編譯檔案控制生成java.exe否則編譯檔案控制生成其他的‘java程式’。比如:j2se/make/java/javac/Makefile(這是javac編譯檔案)中:$(CD) ../../sun/javac ; $(MAKE) $@ RELEASE=$(RELEASE) FULL_VERSION=$(FULL_VERSION)j2se/make/sun/javac/javac/Makefile(由上面Makefile檔案調用)中:JAVA_ARGS = "{ /"-J-ms8m/", /"com.sun.tools.javac.Main/" }"則由同一份java.c代碼生成的javac.exe程式就會直接調用java類方法:com.sun.tools.javac.Main,這樣使其執行起來就像是直接運作的一個exe檔案,而未定義JAVA_ARGS的java.exe程式則會調用傳遞過來參數中的類方法。
從java.c的main入口函數說起
main()函數中前面一段為重新配置設定參數指針的處理。然後調用函數:CreateExecutionEnvironment,該函數主要查找java運作環境的目錄,和jvm.dll這個虛拟機核心動态連接配接庫檔案路徑所在。根據作業系統不同,該函數有不同實作版本,但大體處理邏輯相同,我們看看windows平台該函數的處理(j2se/src/windows/bin/java_md.c)。
CreateExecutionEnvironment函數主要分為三步處理:
a、查找jre路徑。
b、裝載jvm.cfg中指定的虛拟機動态連接配接庫(jvm.dll)參數。
c、取jvm.dll檔案路徑。
實作:
a、查找jre路徑是通過java_md.c中函數:GetJREPath實作的。
該函數首先調用GetApplicationHome函數,GetApplicationHome函數調用windowsAPI函數GetModuleFileName取java.exe程式的絕對路徑,以我的jdk安裝路徑為例,為:“D:/java/j2sdk1.4.2_04/bin/java.exe”,然後去掉檔案名取絕對路徑為:“D:/java/j2sdk1.4.2_04/bin”,之後會在去掉最後一級目錄,現在絕對路徑為:“D:/java/j2sdk1.4.2_04”。然後GetJREPath函數繼續判斷剛剛取的路徑+/bin/java.dll組合成的這個java.dll檔案是否存在,如果存在則“D:/java/j2sdk1.4.2_04”為JRE路徑,否則判斷取得的“D:/java/j2sdk1.4.2_04”路徑+/jre/bin/java.dll檔案是否存在,存在則“D:/java/j2sdk1.4.2_04/jre”為JRE路徑。如果上面兩種情況都不存在,則從系統資料庫中去查找(參見函數GetPublicJREHome)。
函數:GetPublicJREHome先查找HKEY_LOCAL_MACHINE/Software/JavaSoft/Java Runtime Environment/CurrentVersion鍵值“目前JRE版本号”,判斷“目前JRE版本号”是否為1.4做為版本号,如果是則取HKEY_LOCAL_MACHINE/Software/JavaSoft/Java Runtime Environment/“目前JRE版本号”/JavaHome的路徑所在為JRE路徑。我的JDK傳回的JRE路徑為:“D:/java/j2sdk1.4.2_04/jre”。
b、裝載jvm.cfg虛拟機動态連接配接庫配置檔案是通過java.c中函數:ReadKnownVMs實作的。
該函數首先組合jvm.cfg檔案的絕對路徑,JRE路徑+/lib+/ARCH(CPU構架)+/jvm.cfgARCH(CPU構架)的判斷是通過java_md.c中GetArch函數判斷的,該函數中windows平台隻有兩種情況:WIN64的‘ia64’,其他情況都為‘i386’。我的為i386是以jvm.cfg
檔案絕對路徑為:“D:/java/j2sdk1.4.2_04/jre/lib/i386/jvm.cfg”。檔案内容如下:
#
# @(#)jvm.cfg 1.7 03/01/23
#
# Copyright 2003 Sun Microsystems, Inc. All rights reserved.
# SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
#
#
#
#
# List of JVMs that can be used as an option to java, javac, etc.
# Order is important -- first in this list is the default JVM.
# NOTE that this both this file and its format are UNSUPPORTED and
# WILL GO AWAY in a future release.
#
# You may also select a JVM in an arbitrary location with the
# "-XXaltjvm=<jvm_dir>" option, but that too is unsupported
# and may not be available in a future release.
#
-client KNOWN
-server KNOWN
-hotspot ALIASED_TO -client
-classic WARN
-native ERROR
-green ERROR
(如果細心的話,我們會發現在JDK目錄中我的為:“D:/java/j2sdk1.4.2_04/jre/bin/client”和“:/java/j2sdk1.4.2_04/jre/bin/server”兩個目錄下都存在jvm.dll檔案。而java正是通過jvm.cfg配置檔案來管理這些不同版本的jvm.dll的。)
ReadKnownVMs函數會将該檔案中的配置内容讀入到一個JVM配置結構的全局變量中,該函數首先跳過注釋(以‘#’開始的行),然後讀取以‘-’開始的行指定的jvm參數,每一行為一個jvm資訊,第一部分為jvm虛拟機名稱,第二部分為配置參數,比如行:“-client KNOWN”則“-client”為虛拟機名稱,而“KNOWN”為配置類型參數,“KNOWN”表示該虛拟機的jvm.dll存在,而“LIASED_TO”表示為另一個jvm.dll的别名,“WARN”表示該虛拟機的jvm.dll不存在但運作時會用其他存在的jvm.dll替代執行,而“ERROR”同樣表示該類虛拟機的jvm.dll不存在且運作時不會找存在的jvm.dll替代而直接抛出錯誤資訊。
在運作java程式時指定使用那個虛拟機的判斷是由java.c中函數:CheckJvmType判斷,該函數會檢查java運作參數中是否有指定jvm的參數,然後從ReadKnownVMs函數讀取的jvm.cfg資料結構中去查找,進而指定不同的jvm類型(最終導緻裝載不同jvm.dll)。有兩種方法可以指定jvm類型,一種按照jvm.cfg檔案中的jvm名稱指定,第二種方法是直接指定,它們執行的方法分别是“java -J<jvm.cfg中jvm名稱>”、“java -XXaltjvm=<jvm類型名稱>”或“java -J-XXaltjvm=<jvm類型名稱>”。如果是第一種參數傳遞方
式,CheckJvmType函數會取參數‘-J’後面的jvm名稱,然後從已知的jvm配置參數中查找如果找到同名的則去掉該jvm名稱前的‘-’直接傳回該值;而第二種方法,會直接傳回“-XXaltjvm=”或“-J-XXaltjvm=”後面的jvm類型名稱;如果在運作java時未指定
上面兩種方法中的任一一種參數,CheckJvmType會取配置檔案中第一個配置中的jvm名稱,去掉名稱前面的‘-’傳回該值。heckJvmType函數的這個傳回值會在下面的函數中彙同jre路徑組合成jvm.dll的絕對路徑。
比如:如果在運作java程式時使用“java -J-client test”則ReadKnownVMs會讀取參數“-client”然後查找jvm.cfg讀入的參數中是否有jvm名稱為“-client”的,如果有則去掉jvm名稱前的“-”直接傳回“client”;而如果在運作java程式時使用如下參數:
“java -XXaltjvm=D:/java/j2sdk1.4.2_04/jre/bin/client test”,則ReadKnownVMs會直接傳回“D:/java/j2sdk1.4.2_04/jre/bin/client”;如果不帶上面參數執行如:“java test”,因為在jvm.cfg配置檔案中第一個存在的jvm為“-client”,是以函數ReadKnownVMs也會去掉jvm名稱前的“-”傳回“client”。其實這三中情況都是使用的“D:/java/j2sdk1.4.2_04/jre/bin/client/jvm.dll”這個jvm動态連接配接庫處理test這個class的,見下面GetJVMPath函數。
c、取jvm.dll檔案路徑是通過java_md.c中函數:GetJVMPath實作的。
由上面兩步我們已經獲得了JRE路徑和jvm的類型字元串。GetJVMPath函數判斷CheckJvmType傳回的jvm類型字元串中是否包含了‘/’或‘/’如果包含則以該jvm類型字元串+/jvm.dll作為JVM的全路徑,否則以JRE路徑+/bin+/jvm類型字元串+/jvm.dll作為JVM的全路徑。
看看上面的例子,
第一種情況“java -J-client test”jvm.dll路徑為:JRE路徑+/bin+/jvm類型字元串+/jvm.dll 按照我的JDK路徑則為:“D:/java/j2sdk1.4.2_04/jre”+“/bin”+“/client”+“/jvm.dll”。
第二種情況“java -Xaltjvm=D:/java/j2sdk1.4.2_04/jre/bin/client test”路徑為:jvm類型字元串+/jvm.dll即為:“:/java/j2sdk1.4.2_04/jre/bin/client”+“/jvm.dll”
第三種情況“java test”為:“D:/java/j2sdk1.4.2_04/jre”+“/bin”+“/client”+“/jvm.dll”與情況一相同。
是以這三種情況都是調用的jvm動态連接配接庫“D:/java/j2sdk1.4.2_04/jre/bin/client/jvm.dll”處理test類的。
我們來進一步驗證一下:
打開cmd控制台:
設定java裝載調試
E:/work/java_research>set _JAVA_LAUNCHER_DEBUG=1
情況一
E:/work/java_research>java -J-client test.ScanDirectory
----_JAVA_LAUNCHER_DEBUG----
JRE path is D:/java/j2sdk1.4.2_04/jre
jvm.cfg[0] = ->-client<-
jvm.cfg[1] = ->-server<-
jvm.cfg[2] = ->-hotspot<-
jvm.cfg[3] = ->-classic<-
jvm.cfg[4] = ->-native<-
jvm.cfg[5] = ->-green<-
299 micro seconds to parse jvm.cfg
JVM path is D:/java/j2sdk1.4.2_04/jre/bin/client/jvm.dll
2897 micro seconds to LoadJavaVM
JavaVM args:
version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 2
option[ 0] = '-Djava.class.path=.'
option[ 1] = '-Dsun.java.command=test.ScanDirectory'
50001 micro seconds to InitializeJVM
Main-Class is 'test.ScanDirectory'
Apps' argc is 0
10208 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----
usage: java test.ScanDirectory DIR [output file]
情況二
E:/work/java_research>java -XXaltjvm=D:/java/j2sdk1.4.2_04/jre/bin/client test.ScanDirectory
----_JAVA_LAUNCHER_DEBUG----
JRE path is D:/java/j2sdk1.4.2_04/jre
jvm.cfg[0] = ->-client<-
jvm.cfg[1] = ->-server<-
jvm.cfg[2] = ->-hotspot<-
jvm.cfg[3] = ->-classic<-
jvm.cfg[4] = ->-native<-
jvm.cfg[5] = ->-green<-
386 micro seconds to parse jvm.cfg
JVM path is D:/java/j2sdk1.4.2_04/jre/bin/client/jvm.dll
2795 micro seconds to LoadJavaVM
JavaVM args:
version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 2
option[ 0] = '-Djava.class.path=.'
option[ 1] = '-Dsun.java.command=test.ScanDirectory'
49978 micro seconds to InitializeJVM
Main-Class is 'test.ScanDirectory'
Apps' argc is 0
9598 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----
usage: java test.ScanDirectory DIR [output file]
情況三
E:/work/java_research>java test.ScanDirectory
----_JAVA_LAUNCHER_DEBUG----
JRE path is D:/java/j2sdk1.4.2_04/jre
jvm.cfg[0] = ->-client<-
jvm.cfg[1] = ->-server<-
jvm.cfg[2] = ->-hotspot<-
jvm.cfg[3] = ->-classic<-
jvm.cfg[4] = ->-native<-
jvm.cfg[5] = ->-green<-
381 micro seconds to parse jvm.cfg
JVM path is D:/java/j2sdk1.4.2_04/jre/bin/client/jvm.dll
3038 micro seconds to LoadJavaVM
JavaVM args:
version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 2
option[ 0] = '-Djava.class.path=.'
option[ 1] = '-Dsun.java.command=test.ScanDirectory'
50080 micro seconds to InitializeJVM
Main-Class is 'test.ScanDirectory'
Apps' argc is 0
10215 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----
usage: java test.ScanDirectory DIR [output file]
三個的JVM路徑都為:
JVM path is D:/java/j2sdk1.4.2_04/jre/bin/client/jvm.dll
其他情況
E:/work/java_research>java -J-server test.ScanDirectory
----_JAVA_LAUNCHER_DEBUG----
JRE path is D:/java/j2sdk1.4.2_04/jre
jvm.cfg[0] = ->-client<-
jvm.cfg[1] = ->-server<-
jvm.cfg[2] = ->-hotspot<-
jvm.cfg[3] = ->-classic<-
jvm.cfg[4] = ->-native<-
jvm.cfg[5] = ->-green<-
377 micro seconds to parse jvm.cfg
JVM path is D:/java/j2sdk1.4.2_04/jre/bin/server/jvm.dll
2985 micro seconds to LoadJavaVM
JavaVM args:
version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 2
option[ 0] = '-Djava.class.path=.'
option[ 1] = '-Dsun.java.command=test.ScanDirectory'
62382 micro seconds to InitializeJVM
Main-Class is 'test.ScanDirectory'
Apps' argc is 0
12413 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----
usage: java test.ScanDirectory DIR [output file]
E:/work/java_research>java -XXaltjvm=D:/java/j2sdk1.4.2_04/jre/bin/server test.ScanDirectory
----_JAVA_LAUNCHER_DEBUG----
JRE path is D:/java/j2sdk1.4.2_04/jre
jvm.cfg[0] = ->-client<-
jvm.cfg[1] = ->-server<-
jvm.cfg[2] = ->-hotspot<-
jvm.cfg[3] = ->-classic<-
jvm.cfg[4] = ->-native<-
jvm.cfg[5] = ->-green<-
376 micro seconds to parse jvm.cfg
JVM path is D:/java/j2sdk1.4.2_04/jre/bin/server/jvm.dll
2937 micro seconds to LoadJavaVM
JavaVM args:
version 0x00010002, ignoreUnrecognized is JNI_FALSE, nOptions is 2
option[ 0] = '-Djava.class.path=.'
option[ 1] = '-Dsun.java.command=test.ScanDirectory'
62725 micro seconds to InitializeJVM
Main-Class is 'test.ScanDirectory'
Apps' argc is 0
8942 micro seconds to load main class
----_JAVA_LAUNCHER_DEBUG----
usage: java test.ScanDirectory DIR [output file]
由上面可以看出,如果我們安裝了多個jdk或jre版本的話,使用“java -XXaltjvm=”可以通過絕對路徑指定到其他版本的jvm.dll上去,至于能不能運作還有待測試。
我們下面回到java.c的main函數中看看上面找到的jvm.dll是如何裝載挂接執行的。
該操作大緻分為三步:
a、裝載jvm.dll動态連接配接庫。
b、初始化jvm.dll并挂接到JNIEnv(JNI調用接口)執行個體。
c、調用JNIEnv執行個體裝載并處理class類。
實作:
a、裝載jvm.dll動态連接配接庫是由main函數調用java_md.c中LoadJavaVM函數實作的。main函數首先構造了一個InvocationFunctions結構的局部變量,InvocationFunctions結構有兩個函數指針:
typedef struct {
CreateJavaVM_t CreateJavaVM;
GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;
} InvocationFunctions;
函數LoadJavaVM中先調用windows API函數:LoadLibrary裝載jvm.dll動态連接配接庫,之後将jvm.dll中的導出函數JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs挂接到InvocationFunctions變量的CreateJavaVM和GetDefaultJavaVMInitArgs函數指針變量上。jvm.dll的裝載工作宣告完成。
b、初始化jvm.dll并挂接到JNIEnv(JNI調用接口)執行個體是通過java.c中函數:InitializeJVM完成的。main方法中首先定義了一個JNIEnv結構的指針,JNIEnv結構中定義了許多與裝載class類檔案、查找類方法、調用類方法有關的函數指針變量。InitializeJVM會調用上面以挂接jvm.dll中JNI_CreateJavaVM的InvocationFunctions結構變量的CreateJavaVM方法,即調用jvm.dll中函數JNI_CreateJavaVM,該函數會将JNIEnv結構的執行個體傳回到main中的JNIEnv結構的指針上。這樣main中的JNIEnv指針擷取了JNIEnv執行個體後,就可以開始對class檔案進行處理了。
c、調用JNIEnv執行個體裝載并處理class類。
a)如果是執行jar包。
如果執行的是一個jar包的話,main函數會調用java.c中的函數:GetMainClassName,該函數使用JNIEnv執行個體構造并調用java類:java.util.jar.JarFile中方法getManifest()并從傳回的Manifest對象中取getAttributes("Main-Class")的值,即jar包中檔案:META-INF/MANIFEST.MF指定的Main-Class的主類名作為運作的主類。之後main函數會調用java.c中LoadClass方法裝載該主類(使用JNIEnv執行個體的FindClass)。
b)如果是執行class方法。
main函數直接調用java.c中LoadClass方法裝載該類。
然後main函數調用JNIEnv執行個體的GetStaticMethodID方法查找裝載的class主類中“public static void main(String[] args)”方法,并判斷該方法是否為public方法,然後調用JNIEnv執行個體的CallStaticVoidMethod方法調用該java類的main方法。
總結
由上面的代碼分析可以看出幾個問題。
a、為什麼JDK和JRE不一定通過安裝,直接拷到硬碟上,設定path環境變量就可以執行。因為java運作擷取jre路徑的首選方法正是直接通過擷取java.exe絕對路徑來判斷的,如果通過修改系統資料庫選項而不設定path環境變量也可以找到jre路徑所在。修改方法如下:首先我們将java.exe拷到任意目錄下,我的拷到e:/temp下,在cmd中運作:
清空path環境變量
E:/temp>set path=
E:/temp>java
Error opening registry key 'Software/JavaSoft/Java Runtime Environment'
Error: could not find java.dll
Error: could not find Java 2 Runtime Environment.
導入如下系統資料庫檔案(java.reg)
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE/SOFTWARE/JavaSoft]
[HKEY_LOCAL_MACHINE/SOFTWARE/JavaSoft/Java Runtime Environment]
"CurrentVersion"="1.4"
[HKEY_LOCAL_MACHINE/SOFTWARE/JavaSoft/Java Runtime Environment/1.4]
"JavaHome"="D://java//j2sdk1.4.2_04//jre"
再執行顯示執行正常,如下:
E:/temp>java
Usage: java [-options] class [args...]
(to execute a class)
or java [-options] -jar jarfile [args...]
(to execute a jar file)
where options include:
-client to select the "client" VM
-server to select the "server" VM
-hotspot is a synonym for the "client" VM [deprecated]
The default VM is client.
-cp <class search path of directories and zip/jar files>
-classpath <class search path of directories and zip/jar files>
A ; separated list of directories, JAR archives,
and ZIP archives to search for class files.
-D<name>=<value>
set a system property
-verbose[:class|gc|jni]
enable verbose output
-version print product version and exit
-showversion print product version and continue
-? -help print this help message
-X print help on non-standard options
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
enable assertions
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
disable assertions
-esa | -enablesystemassertions
enable system assertions
-dsa | -disablesystemassertions
disable system assertions
b、java.exe是通過jvm.cfg檔案或直接指定jvm.dll路徑來裝載執行java程式的。見上面例子。
c、不同實作版本的jvm.dll必然存在一個名為:JNI_CreateJavaVM的導出函數,java.exe正是通過調用該函數獲得JNIEnv調用接口來裝載執行class類的。這個函數也是我們下一步研究java vm實作技巧的研究出發點。JNI_CreateJavaVM函數位于:hotspot/src/share/vm/prims/jni.cpp檔案中