首先来看看虚拟机这个黑盒子。虚拟机实际上就是jvm.dll,在正式的JDK中有两种版本的虚拟机,对应于Java命令行的-client和-server参数,分别用来运行普通Java程序和服务器程序。-server版本采用了更强大的JIT技术,编译出的代码效率更高;相应地,编译时间也长,所以适合长期运行的服务器程序。实际上从源码还可以编译出另外一个被称为core的版本,这个版本不包括JIT,只有解释器,可以用来学习虚拟机的基本机制。用dumpbin /exports可以看到jvm.dll输出的函数,大约256个输出函数,它们的原型都可以在两个头文件javavm/export/jni.h和javavm/export/jvm.h中找到(写过Java本地库的应该对这两个头文件很熟悉)。我们只需关心下面几个函数,都定义在jni.h中:
JNI_GetDefaultJavaVMInitArgs
JNI_CreateJavaVM JNI_GetCreatedJavaVMs
函数的用途从名字一目了然,不必赘述。一个典型的创建虚拟机的过程如下:
JDK1_1InitArgs args1_1;
JNI_GetDefaultJavaVMInitArgs(&args1_1);
Pseudo Code: Create a thread with stack size of args1_1.javaStackSize
In the new thread:
JavaVMInitArgs args;
args.version = JNI_VERSION_1_2;
args.nOptions = numOptions;
args.options = options;
// 从命令行和环境变量换取的传给虚拟机的信息
JavaVM* vm;
// 可以将它看作创建的虚拟机的标志
JNIEnv* jni;
// 这个指针包括了很多函数指针,称为JNI(Java Native
// API)。接口它是Java和本地代码(C/C++等)的接口。
JNI_CreateJavaVM(&vm, &jni, &args);
mainID = (*jni)->GetStaticMethodID(jni, mainClass, "main", "([Ljava/lang/String;)V");
// 获取Java main方法 (*jni)->CallStaticVoidMethod(jni, mainClass, mainID, mainArgs);
// 运行Java main方法 (*vm)->DestroyJavaVM();
很简单?这就是java、javac、javadoc等工具的主体!
JavaVM里包括了5个函数的指针:
DestroyJavaVM
AttachCurrentThread
DetachCurrentThread
GetEnv
AttachCurrentThreadAsDaemon
这5个函数和之前提到的三个函数在一起被称为Java Invocation API,具体信息可以在这里看到,不再详述。
那么为什么需要JavaVM和JNIEnv两个结构呢?简单的说,是为了解决多线程的问题。Java Invocation API是在一个进程内可以被多个线程同时使用了,它只包括了一些简单的功能。JNIEnv从表面上看来只是一些函数指针,实际上内部有很多数据结构,其中部分数据必须为每个线程保存一份。所以JNIEnv指针是不能在线程之间共享的。这意味着你必须在每个使用JNI的线程里获取该线程的JNIEnv指针。(这就是GetEnv的目的)。
现在来看java.exe等工具的main函数(见jdk/src/share/bin/main.c):
int main(int argc, char ** argv) {
int margc;
char** margv;
const jboolean const_javaw = JNI_FALSE;
margc = argc;
margv = argv;
return JLI_Launch(margc, margv,
sizeof(const_jargs) / sizeof(char *), const_jargs,
// 这是一种常用的获取数组大小的方法。通常用
// sizeof(const_jargs)/sizeof(const_jargs[0])更合适
// 另外一个获取数组大小的办法是用C++模板。
sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
FULL_VERSION, DOT_VERSION,
(const_progname != NULL) ? const_progname : *margv,
(const_launcher != NULL) ? const_launcher : *margv,
(const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
const_cpwildcard, const_javaw, const_ergo_class);
}
可以看到,就是简单地调用JLI_Launch,这个函数属于JLI(Java Launcher Interface?)库,它完成了实际的运行Java虚拟机的任务。不同的工具通过两个参数const_jargs和const_progname来区分。可以在jdk/src/share/bin/defines.h中看到这两个常量的定义:
static const char* const_jargs[] = JAVA_ARGS;
static const char* const_progname = PROGNAME;
编译javac.exe的cl参数包括:
/DJAVA_ARGS="{ \"-J-ms8m\", \"com.sun.tools.javac.Main\", }"
/DPROGNAME="\"javac\""
也就是说javac实际上是在启动Java虚拟机后,运行com.sun.tools.javac.Main。可以预料,javadoc的相应参数为com.sun.tools.javadoc.Main,其它可以此类推。这些工具的代码在langtools\src\share\classes\com\sun\tools下(openjdk没有这部分代码,请看JRL版本的JDK7),均为纯Java代码。
好的,开始分析JLI的代码。
JNI_Launch的实现可以在jdk/src/share/bin/java.[h|cc]中找到。JLI的另外一部分代码是平台相关的,在文件jdk/src/windows/bin/java_md.[h|cc]中。以后我们可以看到很多这种代码组织方式,即平台无关的代码在share目录下,平台相关的在平台名字(如windows,solarias等)的目录下,并在相应的文件名后加上_md后缀。这种模式清晰易读,值得学习。把JNI_Launcher的代码浓缩一下:
JLI_Launch(int argc, char ** argv, /* main argc, argc */
int jargc, const char** jargv, /* java args */
int appclassc, const char** appclassv, /* app classpath */
const char* fullversion, /* full version defined */
const char* dotversion, /* dot version defined */
const char* pname, /* program name */
const char* lname, /* launcher name */
jboolean javaargs, /* JAVA_ARGS */
jboolean cpwildcard, /* classpath wildcard*/
jboolean javaw, /* windows-only javaw */
jint ergo /* ergonomics class policy */
) {
InitLauncher(javaw);
// 调用InitCommonControlsEx和JLI_SetTraceLauncher
CreateExecutionEnvironment(&argc, &argv,
jrepath, sizeof(jrepath),
jvmpath, sizeof(jvmpath),
original_argv);
// 1. 首先查找包括java.dll的目录,即jrepath。
// 假设java.exe所在的上一层目录为JAVA_HOME,
// 那么jrepath可能为JAVA_HOME或者
// JAVA_HOME\jre。
// 2. 打开jrepath\lib\i386\jvm.cfg,这个文件包括了所有
// 支持的虚拟机列表。一个典型的配置为:
// -client KNOWN
// -server KNOWN
// 你可以猜到他们的含义了,:)
// 3. 根据当前命令行参数(-client和-server)决定应该
// 载入的虚拟机(jvm.dll)的全路径,通过jvmpath返回。
LoadJavaVM(jvmpath, &ifn);
// 载入jvm.dll库,并取得JNI_CreateJavaVM和
// JNI_GetDefaultJavaVMInitArgs的指针。
ParseArguments(&argc, &argv, &jarfile, &classname, &ret, jvmpath);
// 顾名思义,解析命令行参数。将命令行参数转换为
// 虚拟机可以理解的JavaVMOption(定义见
// hotspot\src\share\vm\prims\jni.h)
ContinueInNewThread(&ifn, argc, argv, jarfile, classname, ret);
// 调用jvm.dll的JNI_GetDefaultJavaVMInitArgs获取
// javaStackSize参数,然后创建一个初始栈大小为
// javaStackSize的线程,其主函数为JavaMain。主线程的
// 使命到此就结束了,其等待新线程结束后退出。
}
在钻入JavaMain的代码之前,先来看看JLI_SetTraceLauncher,它定义在
jdk\src\share\bin\jli_util.[h|cc]中。相关的还有两个函数:JLI_IsTraceLauncher和JLI_TraceLauncher。JLI_SetTraceLauncher检查环境变量_JAVA_LAUNCHER_DEBUG是否被定义,如是设置全局变量_launcher_debug为JNI_TRUE,表明输出各种调试信息。JLI_TraceLauncher实际上就是printf,在_launcher_debug不为true时直接返回,否则在标准输出上打印调试信息。尝试运行命令"set _JAVA_LAUNCHER_DEBUG=1"后运行"java Hello"。
JavaMain的代码框架如下:
JavaMain(JavaMainArgs *args) {
InitializeJVM(&vm, &env, &ifn);
// 调用JNI_CreateJavaVM创建虚拟机,
// 返回相应的vm和env值。
mainClassName = NewPlatformString(env, classname);
// 调用env的接口构建一个Java String对象。
classname = (char *)(*env)->GetStringUTFChars(env, mainClassName, 0);
// 转换为UTF8编码。
mainClass = LoadClass(env, classname);
// 调用env的FindClass载入main class。
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
// 找到main方法。
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
// 调用main方法。
}
在结束本篇之前,让我们来看看NewPlatformString的实现,即如何在Java虚拟机中创建一个Java String对象。这并不是一个简单的任务:
static jstring NewPlatformString(JNIEnv *env, char *s) {
enc = getPlatformEncoding(env);
ary = (*env)->NewByteArray(env, len);
cls = (*env)->FindClass(env, "java/lang/String");
mid = (*env)->GetMethodID(env, cls, "",
"([BLjava/lang/String;)V");
// 这是找到String(Byte[] ba)方法。
str = (*env)->NewObject(env, cls, mid, ary, enc);
return str;
}
你可以看到这里调用了多个JNI接口来完成创建Java String这个“简单的任务”。下一篇我们将讲述JNI接口的一些细节。
No comments:
Post a Comment