Jni解决了哪些问题?
JNI是Java Native Interface(Java本地接口)的缩写。JNI作为java和操作系统间的一个直接接口,可以通过JNI使得java直接调用操作系统的资源。目前JNI只能通过c/C++实现,因为jni只是对操作系统资源调用的一个桥接过程。所以理论上在windows下只要是dll文件均可以被调用。java代码编译之后是运行在一个jvm里,所以java的任何操作对操作系统而言都是隔着一层虚拟机外壳,这点也正式java的优点,帮助java实现了“Write Once, Run Everywhere”的可移植性。但是使用了jni之后必须要明白这个“Write Once, Run Everywhere”要被打破,必须要实现不同的操作系统的各种jni版本。
JNI的开发调用过程可以用下图来完整表示:
如果这个图表示的不够清楚,可以看下面这个图:
通过jni创建的对象,都是使用jvm的heap空间。比如jstring、jarray等继承于jobject的类内存的分配实际上都是由jvm管理的,并不是使用c的本地内存。如以下代码片段创建了一个String对象,这个对象可以被以jstring的方式传递给jni框架的调用者,最终在jvm的heap里创建了一个内容为"hello world!"的String对象
char *buf = "hello world!";
(*env)->NewStringUTF(env, buf);
在jni代码里,任何jni基本类型的指针或者对象的创建都是通过直接分配内存的,如果创建的对象是被返回给jni调用者的,那么可以不用管理该对象的内存。如果是临时使用,则必须在使用完成之后释放内存
以下情况需要释放内存:
const jbyte *str;
str = (*env)->GetStringUTFChars(env, prompt, NULL);
...
(*env)->ReleaseStringUTFChars(env, prompt, str);
jstring jstr = env->NewStringUTF((*p).sess_id);
...
env->DeleteLocalRef( jstr);
jobject jobj = env->NewObject(clazz,midInit);
return jobj;
所以一般对这类分配空间的操作都是成对出现:
GetStringUTFChars
ReleaseStringUTFChars
GetStringCritical
ReleaseStringCritical
GetStringRegion
SetStringRegion
GetArrayElements
ReleaseArrayElements
GetPrimitiveArrayCritical
ReleasePrimitiveArrayCritical
总结了一下,jni一般有以下一些应用场景
1.高性能 ,在一些情况下因为处理运算量非常大,为了获取高性能,直接使用java是不能胜任的,如:一些图形的处理
2.调用一些硬件的驱动或者一些软件的驱动,比如调用一些外部系统接口的驱动,如:读卡器的驱动,OCI驱动
3.需要使用大内存,远远超过jvm所能分配的内存,如:进程内Cache
4.调用C或者操作系统提供的服务,如:java调用搜索服务,其中搜索是由C/C++实现的,不过这个一般可以设计成更加通用的方式,比如soa的方式
所有这些场景的前提是牺牲了java代码的可移植性,不同的os,甚至版本都需要写不同版本的native实现
java既然运行在jvm之上,那么jvm又是怎么和操作系统交互的呢?jvm是由c语言写成,对java的接口进行了优化,性能非常的好。
这点也让我以为jni接口调用会性能会非常好,实际情况测试表明jni接口的的调用性能是比较差的
以下是在JDK 1.6.0_07-b06版本windows下的测试结果:
1. 准备了两个空方法,分别是java版本和jni版本的helloNothing,方法里面不包含任何逻辑,测试分为10组,每组循环1亿次
HelloNothing h = new HelloNothing();
int times = 100000000; //亿次
// test native method
for (int n = 0; n < 10; n++) {
long t1 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
h.helloNothing1();
}
long t2 = System.currentTimeMillis();
System.out.println("round " + n + ": " + (t2 - t1) + "ms");
}
// test java method
for (int n = 0; n < 10; n++) {
long t1 = System.currentTimeMillis();
for (int i = 0; i < times; i++) {
h.helloNothing();
}
long t2 = System.currentTimeMillis();
System.out.println("round " + n + ": " + (t2 - t1) + "ms");
}
java直接调用 helloNothing():
private void helloNothing1(){
// do nothing
}
round 0: 125ms
round 1: 140ms
round 2: 157ms
round 3: 140ms
round 4: 125ms
round 5: 125ms
round 6: 140ms
round 7: 141ms
round 8: 141ms
round 9: 141ms
平均时间:137 ms 每秒执行:7.2亿次
java调用jni的 helloNothing():
private native void helloNothing();
JNIEXPORT void JNICALL Java_com_taobao_pcache_jni_HelloNothing_helloNothing
(JNIEnv *env, jobject obj)
{
// do nothing
}
测试结果:
round 0: 2453ms
round 1: 2156ms
round 2: 2187ms
round 3: 2156ms
round 4: 2157ms
round 5: 2187ms
round 6: 2156ms
round 7: 2156ms
round 8: 2141ms
round 9: 2156ms
平均时间:2190 ms 每秒执行:0.46亿次
java普通空方法和native空方法的调用效率相差在15倍左右,据说在jdk1.6之前的版本性能更差
2. 增加测试方法的复杂度,在方法里创建并返回一个String对象,测试循环1千万次
java被测试的方法:
private String helloString1(){
return "hello world!";
}
round 0: 328ms
round 1: 344ms
round 2: 328ms
round 3: 328ms
round 4: 344ms
round 5: 328ms
round 6: 328ms
round 7: 328ms
round 8: 344ms
round 9: 343ms
平均时间:334 ms 每秒执行:0.3亿次
native被测试的方法:
JNIEXPORT jstring JNICALL Java_com_taobao_pcache_jni_HelloNothing_helloString
(JNIEnv *env , jobject obj)
{
return (*env)->NewStringUTF(env, "hello world!");
}
测试结果:
round 0: 4234ms
round 1: 4484ms
round 2: 4297ms
round 3: 4140ms
round 4: 4094ms
round 5: 4296ms
round 6: 4532ms
round 7: 4391ms
round 8: 4390ms
round 9: 4406ms
平均时间:4326 ms 每秒执行:0.0231亿次
java普通字符串创建方法和native方法的调用效率相差在13倍左右,根据空方法的调用结果可以发现,通过jni new String对象和java直接new String对象消耗的时间上是差不多的,时间最大的消耗在于java调用jni方法的桥接上 。由于环境不一样,所以以上测试结果只是作为一个大致的参考 。
1.windows下编译dll 命令行:
cl -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 -MD -LD HelloNothing.c -FeHelloNothing.dll
linux下编译成so命令行:
gcc -I/opt/taobao/java/include -I/opt/taobao/java/include/linux -shared -fPIC HelloNothing.c -o libHelloNothing.so
2.dll/so加载问题:
类似下面的错误,表示dll或者so在环境变量路径里不能被找到
java.lang.UnsatisfiedLinkError: no HelloWorld in library path
at java.lang.Runtime.loadLibrary(Runtime.java)
at java.lang.System.loadLibrary(System.java)
at HelloWorld.main(HelloWorld.java)
解决方法:
1.将so或者dll拷贝到path变量指定的路径下
2.在执行的时候指定当前类路径为路径java.library.path的路径:java -Djava.library.path=. HelloWorld
3.如果是tomcat或者jboss则在启动脚本里配置java.library.path变量的路径
3.内存泄露
通过Jni创建的对象全部使用了堆内存,如以下代码片段创建了一个String对象,这个对象可以被以jstring的方式传递给jni框架的调用者,最终在jvm的heap里创建了一个内容为"hello world!"的String对象
char *buf = "hello world!";
(*env)->NewStringUTF(env, buf);
在jni代码里,任何jni对象的创建都是通过直接分配内存的,如果创建的对象是被返回给jni调用者的,那么可以不用管理该对象的内存。如果是临时使用,则必须在使用完成之后释放内存
Java Language Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits