JVM装载过程

简述

    众所周知java.exejava class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dllwindows下,下皆以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.c是什么东西

 

java程序’源代码

 

    所谓‘java程序’,包括jdk中的java.exe\javac.exe\javadoc.exejava.c源代码中通过JAVA_ARGS宏来控制生成的代码,如果该宏没定义则编译文件控制生成java.exe否则编译文件控制生成其他的‘java程序’。比如:j2se\make\java\javac\Makefile(这是javac编译文件)中:$(CD) http://www.cnblogs.com/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_ARGSjava.exe程序则会调用传递过来参数中的类方法。

   

java.cmain入口函数说起

 

    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函数GetModuleFileNamejava.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_04JRE路径,否则判断取得的“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+\ARCHCPU构架)+\jvm.cfgARCHCPU构架)的判断是通过java_md.cGetArch函数判断的,该函数中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.cfgjvm名称>”、“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 testjvm.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]

 

 

    由上面可以看出,如果我们安装了多个jdkjre版本的话,使用“java -XXaltjvm=”可以通过绝对路径指定到其他版本的jvm.dll上去,至于能不能运行还有待测试。

我们下面回到java.cmain函数中看看上面找到的jvm.dll是如何装载挂接执行的。

 

    该操作大致分为三步:

    a、装载jvm.dll动态连接库。

    b、初始化jvm.dll并挂接到JNIEnvJNI调用接口)实例。

    c、调用JNIEnv实例装载并处理class类。

 

    实现:

    a、装载jvm.dll动态连接库是由main函数调用java_md.cLoadJavaVM函数实现的。main函数首先构造了一个InvocationFunctions结构的局部变量,InvocationFunctions结构有两个函数指针:

 

    typedef struct {

        CreateJavaVM_t CreateJavaVM;

        GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs;

    } InvocationFunctions;

 

    函数LoadJavaVM中先调用windows API函数:LoadLibrary装载jvm.dll动态连接库,之后将jvm.dll中的导出函数JNI_CreateJavaVMJNI_GetDefaultJavaVMInitArgs挂接到InvocationFunctions变量的CreateJavaVMGetDefaultJavaVMInitArgs函数指针变量上。jvm.dll的装载工作宣告完成。

 

    b、初始化jvm.dll并挂接到JNIEnvJNI调用接口)实例是通过java.c中函数:InitializeJVM完成的。main方法中首先定义了一个JNIEnv结构的指针,JNIEnv结构中定义了许多与装载class类文件、查找类方法、调用类方法有关的函数指针变量。InitializeJVM会调用上面以挂接jvm.dllJNI_CreateJavaVMInvocationFunctions结构变量的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.cLoadClass方法装载该主类(使用JNIEnv实例的FindClass)。

 

    b)如果是执行class方法。

    main函数直接调用java.cLoadClass方法装载该类。

 

    然后main函数调用JNIEnv实例的GetStaticMethodID方法查找装载的class主类中“public static void main(String[] args)”方法,并判断该方法是否为public方法,然后调用JNIEnv实例的CallStaticVoidMethod方法调用该java类的main方法。

 

总结

    由上面的代码分析可以看出几个问题。

    a、为什么JDKJRE不一定通过安装,直接拷到硬盘上,设置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

 

    bjava.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文件中。

原文地址:https://www.cnblogs.com/yangy608/p/2114905.html