jni2

JNI 调用构造方法和父类实例方法

在前面我们学习到了在 Native 层如何调用 Java 静态方法和实例方法,其中调用实例方法的示例代码中也提到了调用构造函数来实始化一个对象,但没有详细介绍,一带而过了。还没有阅读过的同学请移步《JNI——C/C++ 访问 Java 实例方法和静态方法》阅读。这章详细来介绍下初始一个对象的两种方式,以及如何调用子类对象重写的父类实例方法。

构造方法和父类实例方法

我们先回过一下,在 Java 中实例化一个对象和调用父类实例方法的流程。先看一段代码:

package com.study.jnilearn;
public class Animal {
    public void run() {
        System.out.println("Animal.run...");
    }
}

package com.study.jnilearn;
public class Cat extends Animal {
    @Override
    public void run() {
        System.out.println(name + " Cat.run...");
    }
}

public static void main(String[] args) {
    Animal cat = new Cat("汤姆");
    cat.run();
}

正如你所看到的那样,上面这段代码非常简单,有两个类 Animal 和 Cat,Animal 类中定义了 run 和 getName 两个方法,Cat 继承自 Animal,并重写了父类的 run 方法。在 main 方法中,首先定义了一个 Animal 类型的变量 cat,并指向了 Cat 类的实例对象,然后调用了它的 run 方法。在执行 new Cat(“汤姆”)这段代码时,会先为 Cat 类分配内存空间(所分配的内存空间大小由 Cat 类的成员变量数量决定),然后调用 Cat 的带参构造方法初始化对象。 cat 是 Animal 类型,但它指向的是 Cat 实例对象的引用,而且 Cat 重写了父类的 run 方法,因为调用 run 方法时有多态存在,所以访问的是 Cat 的 run 而非 Animal 的 run,运行后打印的结果为:汤姆 Cat.run…

如果要调用父类的 run 方法,只需在 Cat 的 run 方法中调用 super.run() 即可,相当的简单。

写过 C 或 C++ 的同学应该都有一个很深刻的内存管理概念,栈空间和堆空间,栈空间的内存大小受操作系统限制,由操作系统自动来管理,速度较快,所以在函数中定义的局部变量、函数形参变量都存储在栈空间。操作系统没有限制堆空间的内存大小,只受物理内存的限制,内存需要程序员自己管理。在 C 语言中用 malloc 关键字动态分配的内存和在 C++ 中用 new 创建的对象所分配内存都存储在堆空间,内存使用完之后分别用freedelete/delete[]释放。这里不过多的讨论 C/C++ 内存管理方面的知识,有兴趣的同学请自行百度。做 Java 的童鞋众所周知,写 Java 程序是不需要手动来管理内存的,内存管理那些烦锁的事情全都交由一个叫 GC 的线程来管理(当一个对象没有被其它对象所引用时,该对象就会被 GC 释放)。但我觉得 Java 内部的内存管理原理和 C/C++ 是非常相似的,上例中,Animal cat = new Cat(“汤姆”);局部变量 cat 存放在栈空间上,new Cat (“汤姆”);创建的实例对象存放在堆空间,返回一个内存地址的引用,存储在 cat 变量中。这样就可以通过 cat 变量所指向的引用访问 Cat 实例当中所有可见的成员了。

所以创建一个对象分为 2 步:

  • 为对象分配内存空间
  • 初始化对象(调用对象的构造方法)

下面通过一个示例来了解在 JNI 中是如何调用对象构造方法和父类实例方法的。为了让示例能清晰的体现构造方法和父类实例方法的调用流程,定义了 Animal 和 Cat 两个类,Animal 定义了一个 String 形参的构造方法,一个成员变量 name、两个成员函数 run 和 getName,Cat 继承自 Animal,并重写了 run 方法。在 JNI 中实现创建 Cat 对象的实例,调用 Animal 类的 run 和 getName 方法。代码如下所示。

// Animal.java
package com.study.jnilearn;
public class Animal {

    protected String name;

    public Animal(String name) {
        this.name = name;
        System.out.println("Animal Construct call...");
    }

    public String getName() {
        System.out.println("Animal.getName Call...");
        return this.name;
    }

    public void run() {
        System.out.println("Animal.run...");
    }   
}

// Cat.java
package com.study.jnilearn;
public class Cat extends Animal {

    public Cat(String name) {
        super(name);
        System.out.println("Cat Construct call....");
    }

    @Override
    public String getName() {
        return "My name is " + this.name;
    }

    @Override
    public void run() {
        System.out.println(name + " Cat.run...");
    }
}

// AccessSuperMethod.java
package com.study.jnilearn;
public class AccessSuperMethod {

    public native static void callSuperInstanceMethod(); 

    public static void main(String[] args) {
        callSuperInstanceMethod();
    }

    static {
        System.loadLibrary("AccessSuperMethod");
    }
}

AccessSuperMethod 类是程序的入口,其中定义了一个 native 方法 callSuperInstanceMethod。用 javah 生成的 jni 函数原型如下。

/* Header for class com_study_jnilearn_AccessSuperMethod */

#ifndef _Included_com_study_jnilearn_AccessSuperMethod
#define _Included_com_study_jnilearn_AccessSuperMethod
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_study_jnilearn_AccessSuperMethod
 * Method:    callSuperInstanceMethod
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

实现 Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod 函数,如下所示。

/ AccessSuperMethod.c

#include "com_study_jnilearn_AccessSuperMethod.h"

JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod
  (JNIEnv *env, jclass cls)
{
    jclass cls_cat;
    jclass cls_animal;
    jmethodID mid_cat_init;
    jmethodID mid_run;
    jmethodID mid_getName;
    jstring c_str_name;
    jobject obj_cat;
    const char *name = NULL;

    // 1、获取Cat类的class引用
    cls_cat = (*env)->FindClass(env, "com/study/jnilearn/Cat");
    if (cls_cat == NULL) {
        return;
    }

    // 2、获取Cat的构造方法ID(构造方法的名统一为:<init>)
    mid_cat_init = (*env)->GetMethodID(env, cls_cat, "<init>", "(Ljava/lang/String;)V");
    if (mid_cat_init == NULL) {
        return; // 没有找到只有一个参数为String的构造方法
    }

    // 3、创建一个String对象,作为构造方法的参数
    c_str_name = (*env)->NewStringUTF(env, "汤姆猫");
    if (c_str_name == NULL) {
        return; // 创建字符串失败(内存不够)
    }

    //  4、创建Cat对象的实例(调用对象的构造方法并初始化对象)
    obj_cat = (*env)->NewObject(env,cls_cat, mid_cat_init,c_str_name);
    if (obj_cat == NULL) {
        return;
    }

    //-------------- 5、调用Cat父类Animal的run和getName方法 --------------
    cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal");
    if (cls_animal == NULL) {
        return;
    }

    // 例1: 调用父类的run方法
    mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V");    // 获取父类Animal中run方法的id
    if (mid_run == NULL) {
        return;
    }

    // 注意:obj_cat是Cat的实例,cls_animal是Animal的Class引用,mid_run是Animal类中的方法ID
    (*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);

    // 例2:调用父类的getName方法
    // 获取父类Animal中getName方法的id
    mid_getName = (*env)->GetMethodID(env, cls_animal, "getName", "()Ljava/lang/String;");
    if (mid_getName == NULL) {
        return;
    }

    c_str_name = (*env)->CallNonvirtualObjectMethod(env, obj_cat, cls_animal, mid_getName);
    name = (*env)->GetStringUTFChars(env, c_str_name, NULL);
    printf("In C: Animal Name is %s\n", name);

    // 释放从java层获取到的字符串所分配的内存
    (*env)->ReleaseStringUTFChars(env, c_str_name, name);

quit:
    // 删除局部引用(jobject或jobject的子类才属于引用变量),允许VM释放被局部变量所引用的资源
    (*env)->DeleteLocalRef(env, cls_cat);
    (*env)->DeleteLocalRef(env, cls_animal);
    (*env)->DeleteLocalRef(env, c_str_name);
    (*env)->DeleteLocalRef(env, obj_cat);
}

运行结果

代码讲解 - 调用构造方法

调用构造方法和调用对象的实例方法方式是相似的,传入”< init >”作为方法名查找类的构造方法ID,然后调用JNI函数NewObject调用对象的构造函数初始化对象。如下代码所示。

obj_cat = (*env)->NewObject(env,cls_cat,mid_cat_init,c_str_name);

上述这段代码调用了 JNI 函数 NewObject 创建了 Class 引用的一个实例对象。这个函数做了 2 件事情

  • 创建一个未初始化的对象并分配内存空间
  • 调用对象的构造函数初始化对象。这两步也可以分开进行,为对象分配内存,然后再初始化对象,如下代码所示:
 // 1、创建一个未初始化的对象,并分配内存
 obj_cat = (*env)->AllocObject(env, cls_cat);
 if (obj_cat) {
    // 2、调用对象的构造函数初始化对象
    (*env)->CallNonvirtualVoidMethod(env,obj_cat, cls_cat, mid_cat_init, c_str_name);
    if ((*env)->ExceptionCheck(env)) { // 检查异常
        goto quit;
    }
 }

AllocObject 函数创建的是一个未初始化的对象,后面在用这个对象之前,必须调用CallNonvirtualVoidMethod 调用对象的构造函数初始化该对象。而且在使用时一定要非常小心,确保在一个对象上面,构造函数最多被调用一次。有时,先创建一个初始化的对象,然后在合适的时间再调用构造函数的方式是很有用的。尽管如此,大部分情况下,应该使用 NewObject,尽量避免使用容易出错的 AllocObject/CallNonvirtualVoidMethod 函数。

代码讲解 - 调用父类实例方法

如果一个方法被定义在父类中,在子类中被覆盖,也可以调用父类中的这个实例方法。JNI 提供了一系列函数CallNonvirtualXXXMethod 来支持调用各种返回值类型的实例方法。调用一个定义在父类中的实例方法,须遵循下面的步骤。

使用 GetMethodID 函数从一个指向父类的 Class 引用当中获取方法 ID。

cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal");
if (cls_animal == NULL) {
    return;
}

//例1: 调用父类的run方法
mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V");    // 获取父类Animal中run方法的id
if (mid_run == NULL) {
    return;
}

传入子类对象、父类 Class 引用、父类方法 ID 和参数,并调用 CallNonvirtualVoidMethod、 CallNonvirtualBooleanMethod、CallNonvirtualIntMethod 等一系列函数中的一个。其中CallNonvirtualVoidMethod 也可以被用来调用父类的构造函数。

// 注意:obj_cat是Cat的实例,cls_animal是Animal的Class引用,mid_run是Animal类中的方法ID
(*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run);

其实在开发当中,这种调用父类实例方法的情况是很少遇到的,通常在 JAVA 中可以很简单地做到: super.func();但有些特殊需求也可能会用到,所以知道有这么回事还是很有必要的。

 

JNI 调用性能测试及优化

在前面几章我们学习到了,在 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.6329ms303951367次
1.5312ms320512820次
1.4312ms27233115次

Java 调用 JNI 空函数的性能:

JDK版本Java调Java耗时平均每秒调用次数
1.61531ms65316786次
1.51891ms52882072次
1.43672ms27233115次

从上述测试数据可以看出 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 函数中定义了一个静态变量fid_str用于存储字段的 ID,每次调用函数的时候

 static jfieldID fid_str = NULL;

在代码段

 // 先判断字段ID之前是否已经缓存过,如果已经缓存过则不进行查找
    if (fid_str == NULL) {
        fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");

        // 再次判断是否找到该类的str字段
        if (fid_str == NULL) {
            return;
        }
    }

判断字段 ID 是否已经缓存,如果没有先取出来存到fid_str中,下次再调用的时候该变量已经有值了,不用再去JVM中获取,起到了缓存的作用。

在 Java_com_study_jnilearn_AccessCache_newString 函数中定义了两个变量cls_stringcid_string,分别用于存储 java.lang.String 类的 Class 引用和 String 的构造方法 ID。在使用前会先判断是否已经缓存过,如果没有则调用 JNI 的接口从 JVM 中获取 String 的 Class 引用和构造方法 ID 存储到静态变量当中。下次再调用该函数时就可以直接使用,不需要再去找一次了,也达到了缓存的效果,大家第一反映都会这么认为。但是请注意:cls_string是一个局部引用,与方法和字段 ID 不一样,局部引用在函数结束后会被 JVM 自动释放掉,这时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 的时候,直接使用全局变量当中的即可,调用 Java 的 callback 函数。

(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);

两种缓存方式比较

如果在写 JNI 接口时,不能控制方法和字段所在类的源码的话,用使用时缓存比较合理。但比起类静态初始化时缓存来说,用使用时缓存有一些缺点:

  • 使用前,每次都需要检查是否已经缓存该 ID 或 Class 引用
  • 如果在用使用时缓存的 ID,要注意只要本地代码依赖于这个 ID 的值,那么这个类就不会被 unload。另外一方面,如果缓存发生在静态初始化时,当类被 unload 或 reload 时,ID 会被重新计算。因为,尽量在类静态初始化时就缓存字段 ID、方法 ID 和类的 Class 引用。

 

JNI 局部引用、全局引用和弱全局引用

这篇文章比较偏理论,详细介绍了在编写本地代码时三种引用的使用场景和注意事项。可能看起来有点枯燥,但引用是在 JNI 中最容易出错的一个点,如果使用不当,容易使程序造成内存溢出,程序崩溃等现象。《Android JNI局部引用表溢出》这篇文章是一个 JNI 引用使用不当造成引用表溢出,最终导致程序崩溃的例子。建议看完这篇文章之后,再去看。

做 Java 的朋友都知道,在编码的过程当中,内存管理这一块完全是透明的。new 一个类的实例时,只知道创建完这个类的实例之后,会返回这个实例的一个引用,然后就可以拿着这个引用访问它的所有数据成员了(属性、方法)。完全不用管 JVM 内部是怎么实现的,如何为新创建的对象来申请内存,也不用管对象使用完之后内存是怎么释放的,只需知道有一个垃圾回器在帮忙管理这些事情就 OK 的了。有经验的朋友也许知道启动一个 Java 程序,如果没有手动创建其它线程,默认会有两个线程在跑,一个是 main 线程,另一个就是 GC 线程(负责将一些不再使用的对象回收)。如果你曾经是做 Java 的然后转去做 C++,会感觉很不习惯,在 C++ 中 new 一个对象,使用完了还要做一次 delete 操作,malloc 一次同样也要调用 free 来释放相应的内存,否则你的程序就会有内存泄露了。而且在 C/C++ 中内存还分栈空间和堆空间,其中局部变量、函数形参变量、for 中定义的临时变量所分配的内存空间都是存放在栈空间(而且还要注意大小的限制),用 new 和 malloc 申请的内存都存放在堆空间。但 C/C++ 里的内存管理还远远不止这些,这些只是最基础的内存管理常识。做 Java 的人听到这些肯定会偷乐了,咱写 Java 的时候这些都不用管,全都交给 GC 就万事无优了。手动管理内存虽然麻烦,而且需要特别细心,一不小心就有可能造成内存泄露和野指针访问等程序致命的问题,但凡事都有利弊,手动申请和释放内存对程序的掌握比较灵活,不会受到平台的限制。比如我们写Android程序的时候,内存使用就受Dalivk虚拟机的限制,从最初版本的16~24M,到后来的 32M 到 64M,可能随着以后移动设备物理内存的不大扩大,后面的 Android 版本内存限制可能也会随着提高。但在 C/C++ 这层,就完全不受虚拟机的限制了。比如要在 Android 中要存储一张超高清的图片,刚好这张图片的大小超过了 Dalivk 虚拟机对每个应用的内存大小限制,Java 此时就显得无能为力了,但在C/C++ 看来就是小菜一碟了,malloc(1024102450)。C/C++ 程序员得意的说道,Java 不是说是一门纯面象对象的语言吗,所以除了基本数据类型外,其它任何类型所创建的对象,JVM 所申请的内存都存在堆空间。上面提高到了 GC,是负责回收不再使用的对象,它的全称是 Garbage Collection,也就是所谓的垃圾回收。JVM 会在适当的时机触发 GC 操作,一旦进行 GC 操作,就会将一些不再使用的对象进行回收。那么哪些对象会被认为是不再使用,并且可以被回收的呢?我们来看下面二张图。(注:图摘自博主郭霖的《Android 最佳性能实践(二)——分析内存的使用情况》)

上图当中,每个蓝色的圆圈就代表一个内存当中的对象,而圆圈之间的箭头就是它们的引用关系。这些对象有些是处于活动状态的,而有些就已经不再被使用了。那么 GC 操作会从一个叫作 Roots 的对象开始检查,所有它可以访问到的对象就说明还在使用当中,应该进行保留,而其它的对象就表示已经不再被使用了,如下图所示:

可以看到,目前所有黄色的对象都处于活动状态,仍然会被系统继续保留,而蓝色的对象就会在 GC 操作当中被系统回收掉了,这就是 JVM 执行一次 GC 的简单流程。

上面说的废话好像有点多哈,下面进入正题。通过上面的讨论,大家都知道,如果一个 Java 对象没有被其它成员变量或静态变量所引用的话,就随时有可能会被 GC 回收掉。所以我们在编写本地代码时,要注意从 JVM 中获取到的引用在使用时被 GC 回收的可能性。由于本地代码不能直接通过引用操作 JVM 内部的数据结构,要进行这些操作必须调用相应的 JNI 接口来间接操作所引用的数据结构。JNI 提供了和 Java 相对应的引用类型,供本地代码配合 JNI 接口间接操作 JVM 内部的数据内容使用。如:jobject、jstring、jclass、jarray、jintArray 等。因为我们只通过 JNI 接口操作 JNI 提供的引用类型数据结构,而且每个 JVM 都实现了 JNI 规范相应的接口,所以我们不必担心特定 JVM 中对象的存储方式和内部数据结构等信息,我们只需要学习 JNI 中三种不同的引用即可。

由于 Java 程序运行在虚拟机中的这个特点,在 Java 中创建的对象、定义的变量和方法,内部对象的数据结构是怎么定义的,只有 JVM 自己知道。如果我们在 C/C++ 中想要访问 Java 中对象的属性和方法时,是不能够直接操作 JVM 内部 Java 对象的数据结构的。想要在 C/C++ 中正确的访问 Java 的数据结构,JVM 就必须有一套规则来约束 C/C++ 与 Java 互相访问的机制,所以才有了 JNI 规范,JNI 规范定义了一系列接口,任何实现了这套 JNI 接口的 Java 虚拟机,C/C++ 就可以通过调用这一系列接口来间接的访问 Java 中的数据结构。比如前面文章中学习到的常用 JNI 接口有:GetStringUTFChars(从 Java 虚拟机中获取一个字符串)、ReleaseStringUTFChars(释放从 JVM 中获取字符串所分配的内存空间)、NewStringUTF、GetArrayLength、GetFieldID、GetMethodID、FindClass 等。

三种引用简介及区别

在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。区别如下:

局部引用

通过 NewLocalRef 和各种 JNI 接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止 GC 回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM 自动释放,或调用 DeleteLocalRef 释放。(*env)->DeleteLocalRef(env,local_ref)

jclass cls_string = (*env)->FindClass(env, "java/lang/String");
jcharArray charArr = (*env)->NewCharArray(env, len);
jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj);   // 通过NewLocalRef函数创建
...

全局引用

调用 NewGlobalRef 基于局部引用创建,会阻 GC 回收所引用的对象。可以跨方法、跨线程使用。JVM 不会自动释放,必须调用 DeleteGlobalRef 手动释放。(*env)->DeleteGlobalRef(env,g_cls_string)

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewGlobalRef(env,cls_string);
}

弱全局引用

调用 NewWeakGlobalRef 基于局部引用或全局引用创建,不会阻止 GC 回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在 JVM 认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef 手动释放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);
}

局部引用

局部引用也称本地引用,通常是在函数中创建并使用。会阻止 GC 回收所引用的对象。比如,调用 NewObject 接口创建一个新的对象实例并返回一个对这个对象的局部引用。局部引用只有在创建它的本地方法返回前有效,本地方法返回到 Java 层之后,如果 Java 层没有对返回的局部引用使用的话,局部引用就会被 JVM 自动释放。你可能会为了提高程序的性能,在函数中将局部引用存储在静态变量中缓存起来,供下次调用时使用。这种方式是错误的,因为函数返回后局部引很可能马上就会被释放掉,静态变量中存储的就是一个被释放后的内存地址,成了一个野针对,下次再使用的时候就会造成非法地址的访问,使程序崩溃。请看下面一个例子,错误的缓存了 String 的 Class 引用。

/*错误的局部引用*/
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;
        }
    }

   //省略额外的代码.......
    elemArray = (*env)->NewCharArray(env, len);
    // ....
    j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
    // 释放局部引用
    (*env)->DeleteLocalRef(env, elemArray);
    return j_str;
}

上面代码中,我们省略了和我们讨论无关的代码。因为 FindClass 返回一个对 java.lang.String 对象的局部引用,上面代码中缓存 cls_string 做法是错误的。假设一个本地方法 C.f 调用了 newString。

JNIEXPORT jstring JNICALL
 Java_C_f(JNIEnv *env, jobject this)
 {
     char *c_str = ...;
     ...
     return newString(c_str);
}

Java_com_study_jnilearn_AccessCache_newString 下面简称 newString。

C.f 方法返回后,JVM 会释放在这个方法执行期间创建的所有局部引用,也包含对 String 的 Class 引用cls_string。当再次调用 newString 时,newString 所指向引用的内存空间已经被释放,成为了一个野指针,再访问这个指针的引用时,会导致因非法的内存访问造成程序崩溃。

...
... = C.f(); // 第一次调是OK的
... = C.f(); // 第二次调用时,访问的是一个无效的引用.
...

释放局部引用

释放一个局部引用有两种方式,一个是本地方法执行完毕后 JVM 自动释放,另外一个是自己调用 DeleteLocalRef 手动释放。既然 JVM 会在函数返回后会自动释放所有局部引用,为什么还需要手动释放呢?大部分情况下,我们在实现一个本地方法时不必担心局部引用的释放问题,函数被调用完成后,JVM 会自动释放函数中创建的所有局部引用。尽管如此,以下几种情况下,为了避免内存溢出,我们应该手动释放局部引用。

JNI 会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。经测试,Android 上的 JNI 局部引用表最大数量是 512 个。当我们在实现一个本地方法时,可能需要创建大量的局部引用,如果没有及时释放,就有可能导致 JNI 局部引用表的溢出,所以,在不需要局部引用时就立即调用 DeleteLocalRef 手动删除。比如,在下面的代码中,本地代码遍历一个特别大的字符串数组,每遍历一个元素,都会创建一个局部引用,当对使用完这个元素的局部引用时,就应该马上手动释放它。

for (i = 0; i < len; i++) {
     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
     ... /* 使用jstr */
     (*env)->DeleteLocalRef(env, jstr); // 使用完成之后马上释放
}

在编写 JNI 工具函数时,工具函数在程序当中是公用的,被谁调用你是不知道的。上面 newString 这个函数演示了怎么样在工具函数中使用完局部引用后,调用 DeleteLocalRef 删除。不这样做的话,每次调用 newString 之后,都会遗留两个引用占用空间(elemArray和cls_string,cls_string 不用 static 缓存的情况下)。

如果你的本地函数不会返回。比如一个接收消息的函数,里面有一个死循环,用于等待别人发送消息过来while(true) { if (有新的消息) { 处理之。。。。} else { 等待新的消息。。。}}。如果在消息循环当中创建的引用你不显示删除,很快将会造成 JVM 局部引用表溢出。

局部引用会阻止所引用的对象被 GC 回收。比如你写的一个本地函数中刚开始需要访问一个大对象,因此一开始就创建了一个对这个对象的引用,但在函数返回前会有一个大量的非常复杂的计算过程,而在这个计算过程当中是不需要前面创建的那个大对象的引用的。但是,在计算的过程当中,如果这个大对象的引用还没有被释放的话,会阻止 GC 回收这个对象,内存一直占用者,造成资源的浪费。所以这种情况下,在进行复杂计算之前就应该把引用给释放了,以免不必要的资源浪费。

/* 假如这是一个本地方法实现 */
JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
   lref = ...              /* lref引用的是一个大的Java对象 */
   ...                     /* 在这里已经处理完业务逻辑后,这个对象已经使用完了 */
   (*env)->DeleteLocalRef(env, lref); /* 及时删除这个对这个大对象的引用,GC就可以对它回收,并释放相应的资源*/
   lengthyComputation();   /* 在里有个比较耗时的计算过程 */
   return;                 /* 计算完成之后,函数返回之前所有引用都已经释放 */
}

管理局部引用

JNI 提供了一系列函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI 规范指出,任何实现 JNI 规范的 JVM,必须确保每个本地函数至少可以创建 16 个局部引用(可以理解为虚拟机默认支持创建 16 个局部引用)。实际经验表明,这个数量已经满足大多数不需要和 JVM 中内部对象有太多交互的本地方函数。如果需要创建更多的引用,可以通过调用 EnsureLocalCapacity 函数,确保在当前线程中创建指定数量的局部引用,如果创建成功则返回 0,否则创建失败,并抛出 OutOfMemoryError 异常。EnsureLocalCapacity 这个函数是 1.2 以上版本才提供的,为了向下兼容,在编译的时候,如果申请创建的局部引用超过了本地引用的最大容量,在运行时 JVM 会调用 FatalError 函数使程序强制退出。在开发过程当中,可以为 JVM 添加-verbose:jni参数,在编译的时如果发现本地代码在试图申请过多的引用时,会打印警告信息提示我们要注意。在下面的代码中,遍历数组时会获取每个元素的引用,使用完了之后不手动删除,不考虑内存因素的情况下,它可以为这种创建大量的局部引用提供足够的空间。由于没有及时删除局部引用,因此在函数执行期间,会消耗更多的内存。

/*处理函数逻辑时,确保函数能创建len个局部引用*/
if((*env)->EnsureLocalCapacity(env,len) != 0) {
    ... /*申请len个局部引用的内存空间失败 OutOfMemoryError*/
    return;
}
for(i=0; i < len; i++) {
    jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
    // ... 使用jstr字符串
    /*这里没有删除在for中临时创建的局部引用*/
}

另外,除了 EnsureLocalCapacity 函数可以扩充指定容量的局部引用数量外,我们也可以利用 Push/PopLocalFrame 函数对创建作用范围层层嵌套的局部引用。例如,我们把上面那段处理字符串数组的代码用 Push/PopLocalFrame 函数对重写。

#define N_REFS ... /*最大局部引用数量*/
for (i = 0; i < len; i++) {
    if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
        ... /*内存溢出*/
    }
     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
     ... /* 使用jstr */
     (*env)->PopLocalFrame(env, NULL);
}

PushLocalFrame 为当前函数中需要用到的局部引用创建了一个引用堆栈,(如果之前调用 PushLocalFrame 已经创建了 Frame,在当前的本地引用栈中仍然是有效的)每遍历一次调用(*env)->GetObjectArrayElement(env, arr, i);返回一个局部引用时,JVM 会自动将该引用压入当前局部引用栈中。而 PopLocalFrame 负责销毁栈中所有的引用。这样一来,Push/PopLocalFrame 函数对提供了对局部引用生命周期更方便的管理,而不需要时刻关注获取一个引用后,再调用 DeleteLocalRef 来释放引用。在上面的例子中,如果在处理 jstr 的过程当中又创建了局部引用,则 PopLocalFrame 执行时,这些局部引用将全都会被销毁。在调用 PopLocalFrame 销毁当前 frame 中的所有引用前,如果第二个参数 result 不为空,会由 result 生成一个新的局部引用,再把这个新生成的局部引用存储在上一个 frame 中。请看下面的示例。

// 函数原型
jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result);

jstring other_jstr;
for (i = 0; i < len; i++) {
    if ((*env)->PushLocalFrame(env, N_REFS) != 0) {
        ... /*内存溢出*/
    }
     jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
     ... /* 使用jstr */
     if (i == 2) {
        other_jstr = jstr;
     }
    other_jstr = (*env)->PopLocalFrame(env, other_jstr);  // 销毁局部引用栈前返回指定的引用
}

还要注意的一个问题是,局部引用不能跨线程使用,只在创建它的线程有效。不要试图在一个线程中创建局部引用并存储到全局引用中,然后在另外一个线程中使用。

全局引用

全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,也会阻止它所引用的对象被 GC 回收。与局部引用创建方式不同的是,只能通过 NewGlobalRef 函数创建。下面这个版本的 newString 演示怎么样使用一个全局引用。

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
    // ...
    jstring jstr = NULL;
    static jclass cls_string = NULL;
    if (cls_string == NULL) {
        jclass local_cls_string = (*env)->FindClass(env, "java/lang/String");
        if (cls_string == NULL) {
            return NULL;
        }

        // 将java.lang.String类的Class引用缓存到全局引用当中
        cls_string = (*env)->NewGlobalRef(env, local_cls_string);

        // 删除局部引用
        (*env)->DeleteLocalRef(env, local_cls_string);

        // 再次验证全局引用是否创建成功
        if (cls_string == NULL) {
            return NULL;
        }
    }

    // ....
    return jstr;
}

弱全局引用

弱全局引用使用 NewGlobalWeakRef 创建,使用 DeleteGlobalWeakRef 释放。下面简称弱引用。与全局引用类似,弱引用可以跨方法、线程使用。但与全局引用很重要不同的一点是,弱引用不会阻止 GC 回收它引用的对象。在newString 这个函数中,我们也可以使用弱引用来存储 String 的 Class 引用,因为 java.lang.String 这个类是系统类,永远不会被 GC 回收。当本地代码中缓存的引用不一定要阻止 GC 回收它所指向的对象时,弱引用就是一个最好的选择。假设,一个本地方法mypkg.MyCls.f需要缓存一个指向类mypkg.MyCls2的引用,如果在弱引用中缓存的话,仍然允许mypkg.MyCls2这个类被 unload,因为弱引用不会阻止 GC 回收所引用的对象。请看下面的代码段。

JNIEXPORT void JNICALL
Java_mypkg_MyCls_f(JNIEnv *env, jobject self)
{
    static jclass myCls2 = NULL;
    if (myCls2 == NULL)
    {
        jclass myCls2Local = (*env)->FindClass(env, "mypkg/MyCls2");
        if (myCls2Local == NULL)
        {
            return; /* 没有找到mypkg/MyCls2这个类 */
        }
        myCls2 = NewWeakGlobalRef(env, myCls2Local);
        if (myCls2 == NULL)
        {
            return; /* 内存溢出 */
        }
    }
    ... /* 使用myCls2的引用 */
}

我们假设 MyCls 和 MyCls2 有相同的生命周期(例如,他们可能被相同的类加载器加载),因为弱引用的存在,我们不必担心 MyCls 和它所在的本地代码在被使用时,MyCls2 这个类出现先被 unload,后来又会 preload 的情况。当然,如果真的发生这种情况时(MyCls 和 MyCls2 此时的生命周期不同),我们在使用弱引用时,必须先检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被 GC 给 unload 的类对象。下面马上告诉你怎样检查弱引用是否活动,即引用的比较。

引用比较

给定两个引用(不管是全局、局部还是弱全局引用),我们只需要调用 IsSameObject 来判断它们两个是否指向相同的对象。例如:(*env)->IsSameObject(env, obj1, obj2),如果 obj1 和 obj2 指向相同的对象,则返回 JNI_TRUE(或者 1),否则返回 JNI_FALSE(或者 0)。有一个特殊的引用需要注意:NULL,JNI 中的 NULL 引用指向 JVM 中的 null 对象。如果 obj 是一个局部或全局引用,使用(*env)->IsSameObject(env, obj, NULL) 或者obj == NULL 来判断 obj 是否指向一个 null 对象即可。但需要注意的是,IsSameObject 用于弱全局引用与 NULL 比较时,返回值的意义是不同于局部引用和全局引用的。

jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);
jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);
// ... 业务逻辑处理
jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL);

在上面的 IsSameObject 调用中,如果 g_obj_ref 指向的引用已经被回收,会返回 JNI_TRUE,如果 wobj 仍然指向一个活动对象,会返回 JNI_FALSE。

释放全局引用

每一个 JNI 引用被建立时,除了它所指向的 JVM 中对象的引用需要占用一定的内存空间外,引用本身也会消耗掉一个数量的内存空间。作为一个优秀的程序员,我们应该对程序在一个给定的时间段内使用的引用数量要十分小心。短时间内创建大量而没有被立即回收的引用很可能就会导致内存溢出。     当我们的本地代码不再需要一个全局引用时,应该马上调用 DeleteGlobalRef 来释放它。如果不手动调用这个函数,即使这个对象已经没用了,JVM 也不会回收这个全局引用所指向的对象。      同样,当我们的本地代码不再需要一个弱全局引用时,也应该调用 DeleteWeakGlobalRef 来释放它,如果不手动调用这个函数来释放所指向的对象,JVM 仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。

管理引用的规则

前面对三种引用已做了一个全面的介绍,下面来总结一下引用的管理规则和使用时的一些注意事项,使用好引用的目的就是为了减少内存使用和对象被引用保持而不能释放,造成内存浪费。所以在开发当中要特别小心!

通常情况下,有两种本地代码使用引用时要注意:

  • 直接实现Java层声明的native函数的本地代码 当编写这类本地代码时,要当心不要造成全局引用和弱引用的累加,因为本地方法执行完毕后,这两种引用不会被自动释放。
  • 被用在任何环境下的工具函数。例如:方法调用、属性访问和异常处理的工具函数等。

编写工具函数的本地代码时,要当心不要在函数的调用轨迹上遗漏任何的局部引用,因为工具函数被调用的场合和次数是不确定的,一量被大量调用,就很有可能造成内存溢出。所以在编写工具函数时,请遵守下面的规则:

  • 一个返回值为基本类型的工具函数被调用时,它决不能造成局部、全局、弱全局引用被回收的累加。
  • 当一个返回值为引用类型的工具函数被调用时,它除了返回的引用以外,它决不能造成其它局部、全局、弱引用的累加。

对于工具函数来说,为了使用缓存技术而创建一些全局引用或者弱全局引用是正常的。如果一个工具函数返回的是一个引用,我们应该写好注释详细说明返回引用的类型,以便于使用者更好的管理它们。下面的代码中,频繁地调用工具函数 GetInfoString,我们需要知道 GetInfoString 返回引用的类型是什么,以便于每次使用完成后调用相应的 JNI 函数来释放掉它。

 while (JNI_TRUE) {
     jstring infoString = GetInfoString(info);
     ... /* 处理infoString */
     ??? /* 使用完成之后,调用DeleteLocalRef、DeleteGlobalRef、DeleteWeakGlobalRef哪一个函数来释放这个引用呢?*/
}

函数 NewLocalRef 有时被用来确保一个工具函数返回一个局部引用。我们改造一下 newString 这个函数,演示一下这个函数的用法。下面的 newString 是把一个被频繁调用的字符串“CommonString”缓存在了全局引用里。

JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
{
    static jstring result;
    /* 使用wstrncmp函数比较两个Unicode字符串 */
    if (wstrncmp("CommonString", chars, len) == 0)
    {
        /* 将"CommonString"这个字符串缓存到全局引用中 */
        static jstring cachedString = NULL;
        if (cachedString == NULL)
        {
            /* 先创建"CommonString"这个字符串 */
            jstring cachedStringLocal = ...;
            /* 然后将这个字符串缓存到全局引用中 */
            cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);
        }
        // 基于全局引用创建一个局引用返回,也同样会阻止GC回收所引用的这个对象,因为它们指向的是同一个对象
        return (*env)->NewLocalRef(env, cachedString);  
    }
    ... 
    return result;
}

在管理局部引用的生命周期中,Push/PopLocalFrame 是非常方便且安全的。我们可以在本地函数的入口处调用PushLocalFrame,然后在出口处调用 PopLocalFrame,这样的话,在函数内任何位置创建的局部引用都会被释放。而且,这两个函数是非常高效的,强烈建议使用它们。需要注意的是,如果在函数的入口处调用了PushLocalFrame,记住要在函数所有出口(有 return 语句出现的地方)都要调用 PopLocalFrame。在下面的代码中,对 PushLocalFrame 的调用只有一次,但调用 PopLocalFrame 确有多次,当然你也可以使用 goto 语句来统一处理。

jobject f(JNIEnv *env, ...)
{
    jobject result;
    if ((*env)->PushLocalFrame(env, 10) < 0)
    {
        /* 调用PushLocalFrame获取10个局部引用失败,不需要调用PopLocalFrame */
        return NULL;
    }
    ...
    result = ...; // 创建局部引用result
    if (...)
    {
        /* 返回前先弹出栈顶的frame */
        result = (*env)->PopLocalFrame(env, result);
        return result;
    }
    ...
    result = (*env)->PopLocalFrame(env, result);
    /* 正常返回 */
    return result;
}

上面的代码同样演示了函数 PopLocalFrame 的第二个参数的用法,局部引用 result 一开始在 PushLocalFrame 创建在当前 frame 里面,而把 result 传入 PopLocalFrame 中时,PopLocalFrame 在弹出当前的 frame 前,会由 result 生成一个新的局部引用,再将这个新生成的局部引用存储在上一个 frame 当中。

 

编译C文件:

gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared AccessSuperMethod.c -o libAccessSuperMethod.so

https://blog.csdn.net/xyang81/article/details/45770551

https://blog.csdn.net/xyang81/article/details/44873769

转载于:https://www.cnblogs.com/EMH899/p/10800644.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值