简述
众所周知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) [email protected] 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版本号”,java.exe程序内部本身也有一个标识自身的版本值,如1.2、1.3等。java.exe根据自己内部的版本值和CurrentVersion值相比较,如果发现两个值相等,则取HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\“当前JRE版本号” \JavaHome的路径所在为JRE路径, \“当前JRE版本号” \RuntimeLib的路径为动态链接库地址。 如果java.exe内部版本值和CurrentVersion不一致,则报类似以下的错误: Registry key ‘Software\JavaSoft\Java Runtime Environment\CurrentVersion‘ has value ‘1.2‘, but ‘1.3‘ is required.
b、装载jvm.cfg中指定的虚拟机动态连接库(jvm.dll)参数是通过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”。其主要的内容如下:
- -client KNOWN
- -server KNOWN
- -hotspot ALIASED_TO -client
- -classic WARN
- -native ERROR
- -green ERROR
在我们的jdk目录中jre\bin\server和jre\bin\client都有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替代而直接抛出错误信息。
c、取jvm.dll文件路径是通过java_md.c中函数:GetJVMPath实现的。
在运行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”;如果不带上面参数执行如: “java test”,因为在jvm.cfg配置文件中第一个存在的jvm为“-client”,所以函数 ReadKnownVMs也会去掉jvm名称前的“-”返回“client”。其实这三中情况都是使用“D:\java\j2sdk1.4.2_04\jre\bin\client\jvm.dll”这个jvm动态连接库处理test这个class的,见下面GetJVMPath函数。
GetJVMPath函数判断CheckJvmType 返回的jvm类型字符串中是否包含了‘\’或‘/’如果包含则以该jvm类型字符串+\jvm.dll作为JVM的全路径,否则以JRE路径+\bin+\jvm类型字符串+\jvm.dll作为JVM的全路径。
看看上面的例子,
第一种情况“java -J-client test”
jvm.dll路径为: JRE路径+\bin+\jvm类型字符串+\jvm.dll 即为: “D:\java\j2sdk1.4.2_04\jre”+“\bin”+“\client”+“\jvm.dll”。
第二种情况“java -XXaltjvm=D:\java\j2sdk1.4.2_04\jre\bin\client test”
jvm.dll路径为: jvm类型字符串+\jvm.dll 即为:“D:\java\j2sdk1.4.2_04\jre\bin\client”+“\jvm.dll”
第三种情况“java test”
jvm.dll路径为: JRE路径+\bin+\jvm类型字符串+\jvm.dll 即为: “D:\java\j2sdk1.4.2_04\jre”+“\bin”+“\client”+“\jvm.dll”。
所以这三种情况都是调用的jvm动态连接库“D:\java\
j2sdk1.4.2_04\jre\bin\client\jvm.dll”处理test类的。
由上面可以看出,如果我们安装了多个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方法。