本章篇幅较长,因此分为几天完成,下面是05/27/15完成的。
这一章介绍如何在原生应用中嵌入Java虚拟机。一个Java虚拟机的实现通常是一个原生库,原生应用就可以链接这个库,并且使用Java虚拟机的调用接口加载Java虚拟机。事实上,Java程序的启动命令”java”就是使用一个简单的C语言程序,链接了Java虚拟机实现库。这个命令分析命令行参数,使用虚拟机的调用接口加载Java虚拟机,运行Java程序。
7.1 创建Java虚拟机
我们使用一个例子来介绍。下面的例子是C程序使用调用接口加载Java虚拟机,调用Prog.main()这个Java方法,Prog类定义如下:
public class Prog {
public static void main(String[] args) {
System.out.println("Hello World " + args[0]);
}
}
下面是C语言的代码:
#include <jni.h>
#define PATH_SEPARATOR ':' /* Windows系统时修改为";" */
#define USER_CLASSPATH "." /* Prog.class文件的路径 */
void main() {
JNIEnv *env;
JavaVM *jvm;
jint res;
jclass cls;
jmethodID mid;//Prog.main()方法的ID
jstring jstr;
jclass stringClass;
jobjectArray args;//Prog.main()方法的参数
//如果是Java SDK 1.2
#ifdef JNI_VERSION_1_2
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString = "-Djava.class.path=" USER_CLASSPATH;
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = JNI_TRUE;
/* 创建Java虚拟机 */
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
#else //是Java SDK 1.1
JDK1_1InitArgs vm_args;
char classpath[1024];
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
/* 在系统默认的class文件路径变量中添加USER_CLASSPATH变量 */
sprintf(classpath, "%s%c%s", vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
/* 创建Java虚拟机 */
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif /* JNI_VERSION_1_2 */
if (res < 0) { //创建虚拟机失败
fprintf(stderr, "Can't create Java VMn");
exit(1);
}
//寻找Prog类的引用
cls = (*env)->FindClass(env, "Prog");
if (cls == NULL) {
goto destroy;
}
//获取Prog.main()方法的ID
mid = (*env)->GetStaticMethodID(env, cls, "main", "([Ljava/lang/String;)V");
if (mid == NULL) {
goto destroy;
}
jstr = (*env)->NewStringUTF(env, " from C!");
if (jstr == NULL) {
goto destroy;
}
stringClass = (*env)->FindClass(env, "java/lang/String");
//创建一个新的String类
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
if (args == NULL) {
goto destroy;
}
//调用Prog.main()
(*env)->CallStaticVoidMethod(env, cls, mid, args);
destroy:
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
}
//卸载Java虚拟机
(*jvm)->DestroyJavaVM(jvm);
}
javac Prog.java在当前目录产生Prog.class文件。 编译invoke.c(编译时遇到了问题,见下面的描述):
gcc -L/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/jre/lib/server/ invoke.c -livm在当前目录生成了a.out文件。 运行./a.out,结果如下:
Hello World from C!如果在编译invoke.c时遇到了Undefined Reference "JNI_CreateJavaVM"或者找不到libjvm.dylib库等错误的话,可以参考我的另一篇博文:[解决问题](http://blog.xinspace.name/2015/03/27/原生应用创建java虚拟机时遇到undefined-reference-to-jni_createjavavm/)。 下面的部分是在05/27/15完成的 # 7.2 在Java虚拟机中链接原生应用 Java虚拟机调用接口需要在原生应用程序中(如invoke.c)加载Java虚拟机。如何链接是由你的原生应用是部署在特定实现版本的Java虚拟机还是被设计成能在多个版本的Java虚拟机上运行而决定的。 ## 7.2.1 链接特定版本的Java虚拟机 如果原生应用的设计是依赖于特定实现的Java虚拟机的话,就需要链接这个Java虚拟机实现的库。比如,如果使用JDK 1.1 的MacOS(Linux)版本的虚拟机的话,可以使用下面的命令编译和链接invoke.c:
cc -I<jni.h dir> -L<libjava.dylib dir> -lpthread -ljava invoke.c在上面的命令中,要求指定jni.h头文件所在的目录路径,libjvm.dylib所在目录的路径,链接java库和posix线程库。使用线程库是为了让Java虚拟机支持线程,java库实现了Java虚拟机。 当然,对JDK1.2及以上,MacOS(Linux)版本的虚拟机,命令为:
cc -I<jni.h dir> -L<libjvm.dylib dir> -lpthread invoke.c -ljvm
7.2.2 运行时链接某个版本的Java虚拟机
有时原生应用需要链接不同版本的Java虚拟机,此时就不能像7.2.1小节中所述在编译invoke.c时指定特定的版本了,需要在代码中动态的加载共享库进行链接。比如,可以使用下面的工具函数在原生应用中动态指定共享库的名称,加载并链接:
/ Solaris version /
void JNU_FindCreateJavaVM(char vmlibpath)
{
void *libVM = dlopen(vmlibpath, RTLD_LAZY);
if (libVM == NULL) {
return NULL;
}
return dlsym(libVM, “JNI_CreateJavaVM”);
}
其中,dlopen()函数是加载并链接其参数指定的动态库,dlsym是在链接好的动态库中查找指定的符号,并返回这个符号的地址。dlopen()函数的参数用于指定要加载的动态库名称,可以是libjvm.dylib、jvm或absolute/path/of/libjvm.dylib。
此时,这个原生应用就能够在任意的Java虚拟机上运行,只要用上述工具函数动态的加载指定虚拟机版本即可。
在Windows下,dlopen()函数对应着LoadLibrary()函数,dlsym()函数对应GetProcAddress()函数。
7.3 绑定原生线程
假定有一个多线程的原生应用,比如web服务器。当HTTP请求到来时,web服务器应该创建多个线程同时处理各个请求。可能需要在web服务器中嵌入Java虚拟机,这样服务器的每个线程就能同时使用Java虚拟机完成相应的动作。
一般来说,服务器产生的原生代码的生命周期比Java虚拟机的要短,因此我们就需要把运行原生代码的线程绑定到已经运行了的Java虚拟机中,然后在绑定成功的线程中执行JNI调用,最后从Java虚拟机中解绑定而不会打断其他的绑定线程。
下面用一个例子来介绍如何使用Java虚拟机调用接口将原生线程绑定到已经运行了的Java虚拟机。
#include <jni.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
JavaVM jvm; / Java虚拟机对象指针 */
#define PATH_SEPARATOR ‘:’
#define USER_CLASSPATH “.” / Prog类的class文件路径 /
void thread_fun(void arg)
{
jint res;
jclass cls;
jmethodID mid;
jstring jstr;
jclass stringClass;
jobjectArray args;
JNIEnv env;
char buf[100];
int threadNum = (int)arg;
/ 第三个参数为NULL /
#ifdef JNI_VERSION_1_2 //JDK 1.2
res = (*jvm)->AttachCurrentThread(jvm, (void**)&env, NULL);
#else //JDK 1.1
res = (*jvm)->AttachCurrentThread(jvm, &env, NULL);
#endif
if (res < 0) { //如果绑定失败了
fprintf(stderr, “Attach failedn”);
return;
}
cls = (*env)->FindClass(env, "Prog");//查找Prog类
if (cls == NULL) {
goto detach;
}
mid = (*env)->GetStaticMethodID(env, cls, "main","([Ljava/lang/String;)V");//在Prog类中查找main()方法
if (mid == NULL) {
goto detach;
}
sprintf(buf, " from Thread %d", threadNum);
jstr = (*env)->NewStringUTF(env, buf);
if (jstr == NULL) {
goto detach;
}
stringClass = (*env)->FindClass(env, "java/lang/String");
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);//Prog.main()的参数
if (args == NULL) {
goto detach;
}
(*env)->CallStaticVoidMethod(env, cls, mid, args);//调用Prog.main()
detach:
if ((env)->ExceptionOccurred(env)) {
(env)->ExceptionDescribe(env);
}
(jvm)->DetachCurrentThread(jvm);//Java虚拟机与当前线程解绑定
}
void main() {
JNIEnv env;
int i;
jint res;
pthread_t t[5];
#ifdef JNI_VERSION_1_2 //JDK 1.2
JavaVMInitArgs vm_args;
JavaVMOption options[1];
options[0].optionString = “-Djava.class.path=” USER_CLASSPATH;
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = TRUE;
res = JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args);
#else
JDK1_1InitArgs vm_args;
char classpath[1024];
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs(&vm_args);
sprintf(classpath, “%s%c%s”, vm_args.classpath, PATH_SEPARATOR, USER_CLASSPATH);
vm_args.classpath = classpath;
res = JNI_CreateJavaVM(&jvm, &env, &vm_args);
#endif / JNI_VERSION_1_2 /
if (res < 0) {
fprintf(stderr, “Can’t create Java VMn”);
exit(1);
}
for (i = 0; i < 5; i++)
if(pthread_create(&t[i], NULL, (void )thread_fun, (void )i))
return;
sleep(1000);
(*jvm)->DestroyJavaVM(jvm);
}
上述的程序中,main函数创建并加载了Java虚拟机,创建5个线程,然后调用了DestroyJavaVM()函数。5个线程分别将自己与已经加载的Java虚拟机绑定,分别在Java虚拟机中同时调用Prog.main()方法,并在返回之前与虚拟机解绑定。5个线程都执行完之后,DestroyJavaVM()函数遍返回。在JDK 1.1与JDK 1.2中,DestroyJavaVM()函数总是返回错误值,因此在这里可以忽略它。
JNI_AttachCurrentThread()函数的第三个参数为NULL。在JDK 1.2中介绍了JNI_ThreadAttachArgs结构体,用于指定更加细节的线程信息,比如当前Java虚拟机要绑定到的线程的线程组。
当调用DetachCurrentThread()函数时,它释放掉当前线程的所有局部引用。
使用7.2.1节或7.2.2节介绍的创建加载Java虚拟机的方法,运行该程序,结果为:
Hello World from from Thread 0
Hello World from from Thread 3
Hello World from from Thread 2
Hello World from from Thread 4
Hello World from from Thread 1
线程执行的顺序是随机的。
本章的主要内容是关于在线程中嵌入Java虚拟机,讲解了如何创建和卸载Java虚拟机,加载Java虚拟机不同版本的方法,和将Java虚拟机绑定到原生线程的方法。