天天看点

java源代码分析----jvm.dll装载过程

简述

众所周知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文件中。