JNI技术解析第七章 Java虚拟机的调用接口

本章篇幅较长,因此分为几天完成,下面是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);
}
这个C语言程序有点长,下面一点点的分析。 这个原生应用使用条件编译,对Java SDK 1.1与Java SDK 1.2使用不同的创建方法。Java SDK 1.1 使用结构体JDK1_1InitArgs来初始化Java虚拟机的启动参数。虽然Java SDK 1.2也支持这种初始化方法,但是更推荐使用结构体JavaVMInitArgs。常量JNI_VERSION_1_2在Java SDK 1.2的头文件中定义,而没有在Java SDK 1.1中定义。 对于Java SDK 1.1,原生应用首先调用JNI_GetDefaultJavaVMInitArgs()函数获取默认的Java虚拟机的设置,把默认值写入给定的结构体JDK1_1InitArgs变量中,默认值包括这个虚拟机的栈大小、堆大小、默认的class文件路径等。程序也把Prog的class文件路径(即当前路径)添加到了默认的class文件路径之后。 对于Java SDK 1.2,原生应用创建了结构体JavaVMInitArgs。虚拟机的初始化参数存放在JavaVMOption类型的数组变量中。可以在这个数组变量中存放普通的启动命令行参数(如-Djava.class.path=.,设置Prog的class文件路径为当前路径),也可以存放与Java虚拟机实现有关的参数(如-Xmx64m)。设置结构体JavaVMInitArgs的ignoreUnrecognized成员为JNI_TRUE,表明Java虚拟机在启动时可以忽略不识别的与实现相关的命令行参数。 设置完了虚拟机初始化结构体之后,调用JNI_CreateJavaVM()函数加载并初始化Java虚拟机。这个函数更新两个值: 1.更新接口指针jvm为新创建的虚拟机。 2.更新当前线程的JNIEnv*变量env为有效的JNIEnv变量,后续对JNI函数的调用均使用到了这个env变量。 当JNI_CreateJavaVM()函数返回成功后,这个虚拟机就被嵌入到了当前线程中。此时原生应用就像一个原生函数一样在Java虚拟机中运行,因此它就可以使用JNI函数CallStaticVoidMethod()调用Prog.main()方法。 最后,原生应用调用DestroyJavaVM()函数卸载Java虚拟机(注意:在Java SDK 1.1与Java SDK 1.2版本中,不能卸载Java虚拟机实现,DestroyJavaVM()函数总是返回错误)。 编译Prog.java:
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)-&gt;FindClass(env, "Prog");//查找Prog类
if (cls == NULL) {
    goto detach;
}
mid = (*env)-&gt;GetStaticMethodID(env, cls, "main","([Ljava/lang/String;)V");//在Prog类中查找main()方法
if (mid == NULL) {
    goto detach;
}
sprintf(buf, " from Thread %d", threadNum);
jstr = (*env)-&gt;NewStringUTF(env, buf);
if (jstr == NULL) {
    goto detach;
}
stringClass = (*env)-&gt;FindClass(env, "java/lang/String");
args = (*env)-&gt;NewObjectArray(env, 1, stringClass, jstr);//Prog.main()的参数
if (args == NULL) {
    goto detach;
}
(*env)-&gt;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虚拟机绑定到原生线程的方法。