转载请注明出处:http://blog.csdn.net/xyang81/article/details/44279725
在前面几章我们学习到了,在Java中声明一个native方法,然后生成本地接口的函数原型声明,再用C/C++实现这些函数,并生成对应平台的动态共享库放到Java程序的类路径下,最后在Java程序中调用声明的native方法就间接的调用到了C/C++编写的函数了,在C/C++中写的程序可以避开JVM的内存开销过大的限制、处理高性能的计算、调用系统服务等功能。同时也学习到了在本地代码中通过JNI提供的接口,调用Java程序中的任意方法和对象的属性。这是JNI提供的一些优势。但做过Java的童鞋应该都明白,Java程序是运行在JVM上的,所以在Java中调用C/C++或其它语言这种跨语言的接口时,或者说在C/C++代码中通过JNI接口访问Java中对象的方法或属性时,相比Java调用自已的方法,性能是非常低的!!!网上有朋友针对Java调用本地接口,Java调Java方法做了一次详细的测试,来充分说明在享受JNI给程序带来优势的同时,也要接受其所带来的性能开销,下面请看一组测试数据:
Java调用JNI空函数与Java调用Java空方法性能测试
测试环境:JDK1.4.2_19、JDK1.5.0_04和JDK1.6.0_14,测试的重复次数都是一亿次。测试结果的绝对数值意义不大,仅供参考。因为根据JVM和机器性能的不同,测试所产生的数值也会不同,但不管什么机器和JVM应该都能反应同一个问题,Java调用native接口,要比Java调用Java方法性能要低很多。
Java调用Java空方法的性能:
JDK版本 | Java调Java耗时 | 平均每秒调用次数 |
---|---|---|
1.6 | 329ms | 303951367次 |
1.5 | 312ms | 320512820次 |
1.4 | 312ms | 27233115次 |
Java调用JNI空函数的性能:
JDK版本 | Java调JNI耗时 | 平均每秒调用次数 |
---|---|---|
1.6 | 1531ms | 65316786次 |
1.5 | 1891ms | 52882072次 |
1.4 | 3672ms | 27233115次 |
从上述测试数据可以看出JDK版本越高,JNI调用的性能也越好。在JDK1.5中,仅仅是空方法调用,JNI的性能就要比Java内部调用慢将近5倍,而在JDK1.4下更是慢了十多倍。
JNI查找方法ID、字段ID、Class引用性能测试
当我们在本地代码中要访问Java对象的字段或调用它们的方法时,本机代码必须调用FindClass()、GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID()。对于 GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID(),为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。下面对调用JNI接口FindClass查找Class、GetFieldID获取类的字段ID和GetFieldValue获取字段的值的性能做的一个测试。缓存表示只调用一次,不缓存就是每次都调用相应的JNI接口:
java.version = 1.6.0_14
JNI 字段读取 (缓存Class=false ,缓存字段ID=false) 耗时 : 79172 ms 平均每秒 : 1263072
JNI 字段读取 (缓存Class=true ,缓存字段ID=false) 耗时 : 25015 ms 平均每秒 : 3997601
JNI 字段读取 (缓存Class=false ,缓存字段ID=true) 耗时 : 50765 ms 平均每秒 : 1969861
JNI 字段读取 (缓存Class=true ,缓存字段ID=true) 耗时 : 2125 ms 平均每秒 : 47058823
java.version = 1.5.0_04
JNI 字段读取 (缓存Class=false ,缓存字段ID=false) 耗时 : 87109 ms 平均每秒 : 1147987
JNI 字段读取 (缓存Class=true ,缓存字段ID=false) 耗时 : 32031 ms 平均每秒 : 3121975
JNI 字段读取 (缓存Class=false ,缓存字段ID=true) 耗时 : 51657 ms 平均每秒 : 1935846
JNI 字段读取 (缓存Class=true ,缓存字段ID=true) 耗时 : 2187 ms 平均每秒 : 45724737
java.version = 1.4.2_19
JNI 字段读取 (缓存Class=false ,缓存字段ID=false) 耗时 : 97500 ms 平均每秒 : 1025641
JNI 字段读取 (缓存Class=true ,缓存字段ID=false) 耗时 : 38110 ms 平均每秒 : 2623983
JNI 字段读取 (缓存Class=false ,缓存字段ID=true) 耗时 : 55204 ms 平均每秒 : 1811462
JNI 字段读取 (缓存Class=true ,缓存字段ID=true) 耗时 : 4187 ms 平均每秒 : 23883448
根据上面的测试数据得知,查找class和ID(属性和方法ID)消耗的时间比较大。只是读取字段值的时间基本上跟上面的JNI空方法是一个数量级。而如果每次都根据名称查找class和field的话,性能要下降高达40倍。读取一个字段值的性能在百万级上,在交互频繁的JNI应用中是不能忍受的。 消耗时间最多的就是查找class,因此在native里保存class和member id是很有必要的。class和member id在一定范围内是稳定的,但在动态加载的class loader下,保存全局的class要么可能失效,要么可能造成无法卸载classloader,在诸如OSGI框架下的JNI应用还要特别注意这方面的问题。在读取字段值和查找FieldID上,JDK1.4和1.5、1.6的差距是非常明显的。但在最耗时的查找class上,三个版本没有明显差距。
通过上面的测试可以明显的看出,在调用JNI接口获取方法ID、字段ID和Class引用时,如果没用使用缓存的话,性能低至4倍。所以在JNI开发中,合理的使用缓存技术能给程序提高极大的性能。缓存有两种,分别为使用时缓存和类静态初始化时缓存,区别主要在于缓存发生的时刻。
使用时缓存
字段ID、方法ID和Class引用在函数当中使用的同时就缓存起来。下面看一个示例:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
javah生成的头文件:com_study_jnilearn_AccessCache.h
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
实现头文件中的函数:AccessCache.c
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
例1、在Java_com_study_jnilearn_AccessCache_accessField函数中的第8行定义了一个静态变量fid_str用于存储字段的ID,每次调用函数的时候,在第18行先判断字段ID是否已经缓存,如果没有先取出来存到fid_str中,下次再调用的时候该变量已经有值了,不用再去JVM中获取,起到了缓存的作用。
例2、在Java_com_study_jnilearn_AccessCache_newString函数中的53和54行定义了两个变量cls_string和cid_string,分别用于存储java.lang.String类的Class引用和String的构造方法ID。在56行和64行处,使用前会先判断是否已经缓存过,如果没有则调用JNI的接口从JVM中获取String的Class引用和构造方法ID存储到静态变量当中。下次再调用该函数时就可以直接使用,不需要再去找一次了,也达到了缓存的效果,大家第一反映都会这么认为。但是请注意:cls_string是一个局部引用,与方法和字段ID不一样,局部引用在函数结束后会被VM自动释放掉,这时cls_string成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_com_xxxx_newString这个函数的时候,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的。下篇文章会介绍局部引用和全局引用,利用全局引用来防止这种问题,请关注。
类静态初始化缓存
在调用一个类的方法或属性之前,Java虚拟机会先检查该类是否已经加载到内存当中,如果没有则会先加载,然后紧接着会调用该类的静态初始化代码块,所以在静态初始化该类的过程当中计算并缓存该类当中的字段ID和方法ID也是个不错的选择。下面看一个示例:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
JVM加载AccessCache.class到内存当中之后,会调用该类的静态初始化代码块,即static代码块,先调用System.loadLibrary加载动态库到JVM中,紧接着调用native方法initIDs,会调用用到本地函数Java_com_study_jnilearn_AccessCache_initIDs,在该函数中获取需要缓存的ID,然后存入全局变量当中。下次需要用到这些ID的时候,直接使用全局变量当中的即可,如18行当中调用Java的callback函数。
- 1
两种缓存方式比较
如果在写JNI接口时,不能控制方法和字段所在类的源码的话,用使用时缓存比较合理。但比起类静态初始化时缓存来说,用使用时缓存有一些缺点:
1. 使用前,每次都需要检查是否已经缓存该ID或Class引用
2. 如果在用使用时缓存的ID,要注意只要本地代码依赖于这个ID的值,那么这个类就不会被unload。另外一方面,如果缓存发生在静态初始化时,当类被unload或reload时,ID会被重新计算。因为,尽量在类静态初始化时就缓存字段ID、方法ID和类的Class引用。