【Android C++】JNI引用管理(2)

背景

在 Native 代码中有时候会接收 Java 传入的引用类型参数,有时候也会通过 NewObject 方法来创建一个 Java 的引用类型变量。在编写 Native 代码时,要注意这个代表 Java 数据结构类型的引用在使用时会被 GC 回收的可能性。
我们知道,在Java中提供了四个级别的引用:强引用,软引用,弱引用和虚引用:

  1. 强应用:Java中默认声明的就是强引用,比如:​​Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收​​,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OutOfMemoryError,不会去回收。如果想中断强引用与对象之间的联系,可以显示的将强引用赋值为null,这样一来,JVM就可以适时的回收对象了;
  2. 软引用:软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。在 JDK1.2 之后,用java.lang.ref.SoftReference类来表示软引用。
  3. 弱引用:弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用。
private static void testWeakReference() {  
    for (int i = 0; i < 10; i++) {  
      byte\[\] buff = new byte\[1024 \* 1024\];  
      WeakReference<byte\[\]> sr = new WeakReference<>(buff);  
      list.add(sr);  
    }  
      
    System.gc(); //主动通知垃圾回收  
      
    for(int i=0; i < list.size(); i++){  
      Object obj = ((WeakReference) list.get(i)).get();  
      System.out.println(obj);//因为被回收,所以打印结果都是null  
    }  
  } 

  1. 虚引用:虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。而引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现它还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

Native 代码并不能直接通过引用来访问其内部的数据接口,必须要通过调用 JNI 接口来间接操作这些引用对象,就如在之前的系列文章中写的那样。并且 JNI 还提供了和 Java 相对应的引用类型,因此,我们就需要通过管理好这些引用来管理 Java 对象,避免在使用时被 GC 回收了。

引用简介

JNI 提供了三种引用类型:

  • 局部引用
  • 全局引用
  • 弱全局引用

局部引用

局部引用是最常见的一种引用。绝大多数 JNI 函数创建的都是局部引用,比如:NewObject、FindClass、NewObjectArray 函数等等。

局部引用会阻止 GC 回收所引用的对象,同时,它不能在本地函数中跨函数传递,不能跨线程使用。 局部引用在 Native 函数返回后,所引用的对象会被 GC 自动回收,也可以通过 DeleteLocalRef 函数来手动回收。 JNI 调用时缓存字段和方法 ID,第一种方法采用的是使用时缓存,把字段 ID 通过 static 变量缓存起来。

如果把 FindClass 函数创建的局部引用也通过 static 变量缓存起来,那么在函数退出后,局部引用被自动释放了,static 静态变量中存储的就是一个被释放后的内存地址,成为了一个野指针,再次调用时就会引起程序崩溃了。

extern "C"  
 JNIEXPORT jstring JNICALL  
 Java\_com\_qingkouwei\_Demo\_errorCacheUseLocalReference(  
         JNIEnv \*env, jobject instance) {  
     static jmethodID mid = NULL;  
     static jclass cls;  
     // 局部引用不能使用 static 来缓存,否则函数退出后,自动释放,成为野指针,程序 Crash  
     if (cls == NULL) {  
         cls = env->FindClass("java/lang/String");  
         if (cls == NULL) {  
             return NULL;  
         }  
     } else {  
         LOGD("cls is not null but program will crash");  
     }  
     if (mid == NULL) {  
         mid = env->GetMethodID(cls, "<init>", "(\[C)V");  
         if (mid == NULL) {  
             return NULL;  
         }  
     }  
     jcharArray charEleArr = env->NewCharArray(10);  
     const jchar \*j\_char = env->GetStringChars(env->NewStringUTF("LocalReference"), NULL);  
     env->SetCharArrayRegion(charEleArr, 0, 10, j\_char);  
     jstring result = (jstring) env->NewObject(cls, mid, charEleArr);  
     env->DeleteLocalRef(charEleArr);  
     return result;  
 } 

局部引用手动释放时机

局部引用除了自动释放外,还可以通过 DeleteLocalRef 函数手动释放,它一般存在于以下场景中:

  • 当要创建大量的局部引用对象时,会造成 JNI 局部引用表的溢出。 假如有以下代码,则要特别注意,及时释放局部引用,防止溢出。
for (int i = 0; i < len; ++i) {  
         jstring jstr = (\*env)->GetObjectArrayElement(env, arr, i);  
         ... /\* process jstr \*/  
         (\*env)->DeleteLocalRef(env, jstr);  
     } 

  • 编写工具类时,即时释放局部引用。 在编写工具类时,很难知道被调用者具体会是谁,考虑到通用性,完成工具类的任务之后,就要及时释放相应的局部引用,防止被占着内存空间。
  • 不需要返回的 Native 方法,即时释放局部引用。 如果 Native 方法不会返回,那么自动释放局部引用就失效了,这时候就必须要手动释放。比如,在某个一直等待的循环中,如果不及时释放局部引用,很快就会溢出了。
  • 局部引用使用完了就删除,不必要等到函数结尾。 比如,通过局部引用创建了一个大对象,然后这个对象在函数中间就完成了任务,那么就可以早早地通过手动释放了,而不是等到函数的结尾才释放。
管理局部引用

Java 还提供了一些函数来管理局部引用的生命周期:

  • EnsureLocalCapacity
  • NewLocalRef
  • PushLocalFrame
  • PopLocalFrame
EnsureLocalCapacity 函数

JNI 的规范指出,JVM 要确保每个 Native 方法至少可以创建 16 个局部引用,经验表明,16 个局部引用已经足够平常的使用了。

但是,如果要与 JVM 的中对象进行复杂的交互计算,就需要创建更多的局部引用了,这时就需要使用 EnsureLocalCapacity 来确保可以创建指定数量的局部引用,如果创建成功返回 0 ,失败返回小于 0 ,如下代码示例:

// Use EnsureLocalCapacity  
     int len = 20;  
     if (env->EnsureLocalCapacity(len) < 0) {  
         // 创建失败,out of memory  
     }  
     for (int i = 0; i < len; ++i) {  
         jstring  jstr = env->GetObjectArrayElement(arr,i);  
         // 处理 字符串  
         // 创建了足够多的局部引用,这里就不用删除了,显然占用更多的内存  
     } 

引用确保可以创建了足够的局部引用数量,所以在循环处理局部引用时可以不进行删除了,但是显然会消耗更多的内存空间了。

PushLocalFrame 与 PopLocalFrame 函数对

PushLocalFrame 与 PopLocalFrame 是两个配套使用的函数对。

它们可以为局部引用创建一个指定数量内嵌的空间,在这个函数对之间的局部引用都会在这个空间内,直到释放后,所有的局部引用都会被释放掉,不用再担心每一个局部引用的释放问题了。

常见的使用场景就是在循环中:

// Use PushLocalFrame & PopLocalFrame  
     for (int i = 0; i < len; ++i) {  
         if (env->PushLocalFrame(len)) { // 创建指定数据的局部引用空间  
             //out ot memory  
         }  
         jstring jstr = env->GetObjectArrayElement(arr, i);  
         // 处理字符串  
         // 期间创建的局部引用,都会在 PushLocalFrame 创建的局部引用空间中  
         // 调用 PopLocalFrame 直接释放这个空间内的所有局部引用  
         env->PopLocalFrame(NULL);   
     } 

使用 PushLocalFrame & PopLocalFrame 函数对,就可以在期间放心地处理局部引用,最后统一释放掉。

全局引用

全局引用和局部引用一样,也会阻止它所引用的对象被回收。但是它不会在方法返回时被自动释放,必须要通过手动释放才行,而且,全局引用可以跨方法、跨线程使用。

全局引用只能通过 NewGlobalRef函数来创建,然后通过 DeleteGlobalRef 函数来手动释放。
还是上面提到的缓存字段的例子,现在就可以使用全局引用来缓存了。

extern "C"  
 JNIEXPORT jstring JNICALL  
 Java\_com\_qingkouwei\_Demo\_cacheWithGlobalReference(JNIEnv \*env, jobject instance) {  
     static jclass stringClass = NULL;  
     if (stringClass == NULL) {  
         jclass localRefs = env->FindClass("java/lang/String");  
         if (localRefs == NULL) {  
             return NULL;  
         }  
         stringClass = (jclass) env->NewGlobalRef(localRefs);  
         env->DeleteLocalRef(localRefs);  
         if (stringClass == NULL) {  
             return NULL;  
         }  
     } else {  
         LOGD("use stringClass cached");  
     }  
     static jmethodID stringMid = NULL;  
     if (stringMid == NULL) {  
         stringMid = env->GetMethodID(stringClass, "<init>", "(Ljava/lang/String;)V");  
         if (stringMid == NULL) {  
             return NULL;  
         }  
     } else {  
         LOGD("use method cached");  
     }  
     jstring str = env->NewStringUTF("string");  


### 尾声

你不踏出去一步,永远不知道自己潜力有多大,千万别被这个社会套在我们身上的枷锁给捆住了,30岁我不怕,35岁我一样不怕,去做自己想做的事,为自己拼一把吧!不试试怎么知道你不行呢?

改变人生,没有什么捷径可言,这条路需要自己亲自去走一走,只有深入思考,不断反思总结,保持学习的热情,一步一步构建自己完整的知识体系,才是最终的制胜之道,也是程序员应该承担的使命。

>**附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)**

![](https://img-blog.csdnimg.cn/img_convert/add921a2197ca1215da3f185a0ef1f54.webp?x-oss-process=image/format,png)



**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**




**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化学习资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618156601)**

**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值