简述
众所周知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 fcs
community source
release》,可从sun官方网站下载,主要分析的源代码为:
j2se/src/share/bin/java.c
j2se/src/windows/bin/java_md.c
java.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函数调用windows
api函数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
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.cfg
arch(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”和“d:/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存在,而“aliased_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名称,去掉名称前面的‘-’返回该值。checkjvmtype函数的这个返回值会在下面的函数中汇同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”;如果不带上面参数执行如:
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
test”jvm.dll路径为:
jre路径+/bin+/jvm类型字符串+/jvm.dll 按照我的jdk路径则为:
“d:/java/j2sdk1.4.2_04/jre”+“/bin”+“/client”+“/jvm.dll”。
第二种情况“java
test”路径为:
jvm类型字符串+/jvm.dll即为:“d:/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
usage: java
test.scandirectory dir [output file]
情况二
e:/work/java_research>java
386 micro seconds to parse jvm.cfg
2795 micro seconds to
49978 micro seconds to
9598
情况三
381 micro seconds to parse jvm.cfg
3038 micro seconds to
50080 micro seconds to
10215
三个的jvm路径都为:
其他情况
-j-server test.scandirectory
377 micro seconds to parse jvm.cfg
d:/java/j2sdk1.4.2_04/jre/bin/server/jvm.dll
2985 micro seconds to
62382 micro seconds to
12413
-xxaltjvm=d:/java/j2sdk1.4.2_04/jre/bin/server
376 micro seconds to parse jvm.cfg
2937 micro seconds to
62725 micro seconds to
8942
由上面可以看出,如果我们安装了多个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文件进行处理了。
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
environment]
"currentversion"="1.4"
runtime environment/1.4]
"javahome"="d://java//j2sdk1.4.2_04//jre"
再执行显示执行正常,如下:
usage: java [-options] class
[args...]
(to
execute a class)
or java [-options] -jar jarfile
execute a jar file)
where options
include:
-client
to select the "client"
vm
-server to
select the "server"
-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>]
assertions
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
disable
-esa |
-enablesystemassertions
system assertions
-dsa |
-disablesystemassertions
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文件中。