JNI/NDK开发指南(九)——JNI调用性能測试及优化
转载请注明出处: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引用在函数其中使用的同一时候就缓存起来。以下看一个演示样例:
package com.study.jnilearn;
public class AccessCache {
    private String str = "Hello";
    public native void accessField(); // 訪问str成员变量
    public native String newString(char[] chars, int len); // 依据字符数组和指定长度创建String对象
    public static void main(String[] args) {
        AccessCache accessCache = new AccessCache();
        accessCache.nativeMethod();
        char chars[] = new char[7];
        chars[0] = '中';
        chars[1] = '华';
        chars[2] = '人';
        chars[3] = '民';
        chars[4] = '共';
        chars[5] = '和';
        chars[6] = '国';
        String str = accessCache.newString(chars, 6);
        System.out.println(str);
    }
    static {
        System.loadLibrary("AccessCache");
    }
}javah生成的头文件:com_study_jnilearn_AccessCache.h
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_AccessCache */
#ifndef _Included_com_study_jnilearn_AccessCache
#define _Included_com_study_jnilearn_AccessCache
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_AccessCache
 * Method:    accessField
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField(JNIEnv *, jobject);
/*
 * Class:     com_study_jnilearn_AccessCache
 * Method:    newString
 * Signature: ([CI)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString(JNIEnv *, jobject,
jcharArray, jint);
#ifdef __cplusplus
}
#endif
#endif实现头文件里的函数:AccessCache.c
// AccessCache.c
#include "com_study_jnilearn_AccessCache.h"
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField
  (JNIEnv *env, jobject obj)
{
    // 第一次訪问时将字段存到内存数据区,直到程序结束才会释放。能够起到缓存的作用
    static jfieldID fid_str = NULL;
    jclass cls_AccessCache;
    jstring j_str;
    const char *c_str;
    cls_AccessCache = (*env)->GetObjectClass(env, obj); // 获取该对象的Class引用
    if (cls_AccessCache == NULL) {
        return;
    }
    // 先推断字段ID之前是否已经缓存过。假设已经缓存过则不进行查找
    if (fid_str == NULL) {
        fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");
        // 再次推断是否找到该类的str字段
        if (fid_str == NULL) {
            return;
        }
    }
    j_str = (*env)->GetObjectField(env, obj, fid_str);  // 获取字段的值
    c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
    if (c_str == NULL) {
        return; // 内存不够
    }
    printf("In C:\n str = \"%s\"\n", c_str);
    (*env)->ReleaseStringUTFChars(env, j_str, c_str);   // 释放从从JVM新分配字符串的内存空间
    // 改动字段的值
    j_str = (*env)->NewStringUTF(env, "12345");
    if (j_str == NULL) {
        return;
    }
    (*env)->SetObjectField(env, obj, fid_str, j_str);
    // 释放本地引用
    (*env)->DeleteLocalRef(env,cls_AccessCache);
    (*env)->DeleteLocalRef(env,j_str);
}
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
    jcharArray elemArray;
    jchar *chars = NULL;
    jstring j_str = NULL;
    static jclass cls_string = NULL;
    static jmethodID cid_string = NULL;
    // 注意:这里缓存局引用的做法是错误。这里做为一个反面教材提醒大家,以下会说到。
    if (cls_string == NULL) {
        cls_string = (*env)->FindClass(env, "java/lang/String");
        if (cls_string == NULL) {
            return NULL;
        }
    }
    // 缓存String的构造方法ID
    if (cid_string == NULL) {
        cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
        if (cid_string == NULL) {
            return NULL;
        }
    }
    printf("In C array Len: %d\n", len);
    // 创建一个字符数组
    elemArray = (*env)->NewCharArray(env, len);
    if (elemArray == NULL) {
        return NULL;
    }
    // 获取数组的指针引用,注意:不能直接将jcharArray作为SetCharArrayRegion函数最后一个參数
    chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL);
    if (chars == NULL) {
        return NULL;
    }
    // 将Java字符数组中的内容复制指定长度到新的字符数组中
    (*env)->SetCharArrayRegion(env, elemArray, 0, len, chars);
    // 调用String对象的构造方法,创建一个指定字符数组为内容的String对象
    j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
    // 释放本地引用
    (*env)->DeleteLocalRef(env, elemArray);
    return j_str;
}例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也是个不错的选择。以下看一个演示样例:
package com.study.jnilearn;
public class AccessCache {
    public static native void initIDs(); 
    public native void nativeMethod();
    public void callback() {
        System.out.println("AccessCache.callback invoked!");
    }
    public static void main(String[] args) {
        AccessCache accessCache = new AccessCache();
        accessCache.nativeMethod();
    }
    static {
        System.loadLibrary("AccessCache");
        initIDs();
    }
}/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_AccessCache */
#ifndef _Included_com_study_jnilearn_AccessCache
#define _Included_com_study_jnilearn_AccessCache
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_AccessCache
 * Method:    initIDs
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs
  (JNIEnv *, jclass);
/*
 * Class:     com_study_jnilearn_AccessCache
 * Method:    nativeMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod
  (JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif// AccessCache.c
#include "com_study_jnilearn_AccessCache.h"
jmethodID MID_AccessCache_callback;
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs
(JNIEnv *env, jclass cls)
{
    printf("initIDs called!!!\n");
    MID_AccessCache_callback = (*env)->GetMethodID(env,cls,"callback","()V");
}
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod
(JNIEnv *env, jobject obj)
{
    printf("In C Java_com_study_jnilearn_AccessCache_nativeMethod called!!!\n");
    (*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);
}JVM载入AccessCache.class到内存其中之后,会调用该类的静态初始化代码块,即static代码块,先调用System.loadLibrary载入动态库到JVM中。紧接着调用native方法initIDs。会调用用到本地函数Java_com_study_jnilearn_AccessCache_initIDs。在该函数中获取须要缓存的ID。然后存入全局变量其中。下次须要用到这些ID的时候,直接使用全局变量其中的就可以,如18行其中调用Java的callback函数。
(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);两种缓存方式比較
假设在写JNI接口时,不能控制方法和字段所在类的源代码的话,用使用时缓存比較合理。
但比起类静态初始化时缓存来说。用使用时缓存有一些缺点: 
1. 使用前。每次都须要检查是否已经缓存该ID或Class引用 
2. 假设在用使用时缓存的ID。要注意仅仅要本地代码依赖于这个ID的值,那么这个类就不会被unload。
另外一方面。假设缓存发生在静态初始化时,当类被unload或reload时,ID会被又一次计算。由于。尽量在类静态初始化时就缓存字段ID、方法ID和类的Class引用。
JNI/NDK开发指南(九)——JNI调用性能測试及优化的更多相关文章
- JNI/NDK开发指南(开山篇)
		转载请注明出处:http://blog.csdn.net/xyang81/article/details/41759643 相信很多做过Java或Android开发的朋友经常会接触到JNI方面的技术, ... 
- JNI/NDK开发指南(一)—— JNI开发流程及HelloWorld
		转载请注明出处:http://blog.csdn.net/xyang81/article/details/41777471 JNI全称是Java Native Interface(Java本地接口)单 ... 
- JNI/NDK开发指南(二)——JVM查找java native方法的规则
		通过第一篇文章,大家明白了调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出java.lang.Unsatis ... 
- JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用
		转自:http://blog.csdn.net/xyang81/article/details/44657385 这篇文章比较偏理论,详细介绍了在编写本地代码时三种引用的使用场景和注意事项.可能看 ... 
- JNI/NDK开发指南(四)——字符串处理
		转载请注明出处:http://blog.csdn.net/xyang81/article/details/42066665 从第三章中能够看出JNI中的基本类型和Java中的基本类型都是一一相应的,接 ... 
- JNI/NDK开发指南(2)
		1.生成动态库.so,存放于手机的system/lib/中(APP怎样将.so存入该文件夹,奇怪?????),Java层调用JNI的类会运行静态代码System.loadLibrary("* ... 
- JNI/NDK开发指南(三)——JNI数据类型及与Java数据类型的映射关系
		转载请注明出处:http://blog.csdn.net/xyang81/article/details/42047899 当我们在调用一个Java native方法的时候.方法中的參数是怎样传递给C ... 
- Android JNI/NDK开发教程
		JNI/NDK开发指南:http://blog.csdn.net/xyang81/article/details/41759643 
- JNI/NDK开发
		公司的新需求终于解决完了,离测试和发布还有段时间,第一次体验了下没需求没bug的感觉,真是舒爽~然后翻了翻有什么可以学的.无意翻到了Android后期发展的五大趋势.一.性能优化.二.高级UI.三.J ... 
随机推荐
- 洛谷 P1604 B进制星球
			P1604 B进制星球 题目背景 进制题目,而且还是个计算器~~ 题目描述 话说有一天,小Z乘坐宇宙飞船,飞到一个美丽的星球.因为历史的原因,科技在这个美丽的星球上并不很发达,星球上人们普遍采用B(2 ... 
- 第二十四天 框架之痛-Spring MVC(四)
			6月3日,晴."绿树浓阴夏日长. 楼台倒影入池塘. 水晶帘动微风起, 满架蔷薇一院香". 以用户注冊过程为例.我们可能会选择继承AbstractController来实现表单的显示 ... 
- BNU 34974 MATLAB大法好
			题目链接:http://www.bnuoj.com/bnuoj/problem_show.php?pid=34974 MATLAB大法好 Time Limit: 8000ms Memory Limi ... 
- 【Hibernate步步为营】--(一对多映射)之单向关联
			上篇文章讨论了双向关联的一对一映射,用了两个章节,主要是从主键和外键两种关联映射展开具体讨论.双向关联的映射须要在两个映射文件里分别加入相互的相应关系.斌刚在相应的类中加入相应的关联类的属性.这样在一 ... 
- Android——4.2 - 3G移植之路之 APN (五)
			APN,这东西对于刚接触的人来说并非那么好理解.对于3G移植上网不可缺少,这里记录一下. 撰写不易,转载请注明出处:http://blog.csdn.net/jscese/article/detail ... 
- ATL中宏定义offsetofclass的分析
			近日学习ATL,通过对宏定义offsetofclass的解惑过程.顺便分析下虚函数表,以及通过虚函数表调用函数的问题. 1 解开ATL中宏定义offsetofclass的疑惑 #define _ATL ... 
- Track Active Item in Solution Explorer
			Tools-->Options-->Projects and Solutions-->Track Active Item in Solution Explorer 
- 123.static静态函数和函数模板
			#include <iostream> using namespace std; //static成员,每个类型都会实例化,创建一个变量,类型一致则共享,否则不共享 template &l ... 
- 开源3D游戏引擎Irrlicht简介
			Irrlicht简介 Irrlicht在国内也被叫做"鬼火"引擎,是一款用C++编写的开放源代码的高性能游戏引擎.而且是跨平台的,具有很好的移植性,Irrlicht支持OpenGl ... 
- windows 手动添加服务
			windows 手动添加服务方法一:修改注册表 在注册表编辑器,展开分支"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services" ... 
