NDK开发之引用(局部引用,全局引用,虚全局引用)

时间:2022-06-20 15:12:10

1、先引出我遇到的一个问题(我觉得先写问题,这样印象更深刻一点):

Android Java 层在调用本地 jni 代码的时候, 会维护一个局部引用表(最大长度是 512), 一般 jni 函数调用结束后, jvm 会释放这个引用, 如果是简单的函数不注意这些问题,让他自己释放,基本是没有什么问题, 但是如果函数里面有循环的操作的话,那么程序就会有崩溃的隐患, 比如 之前我在项目里面就有在一个 jni 方法里面,将 native 层的一个队列封装成 Java 层的 List,然后返回给 Java 层使用, 这样的话,不可避免的在本地方法里面有循环操作,而自己恰恰有些小细节没有注意, 忘了删除一些局部应用,开始测少量数据的时候一点问题没有, 后来数据规模大了, 直接就崩了,还好 log 日志里面打印出了崩溃日志,然后知道是代码里面是有引用没有释放, 具体日志是这样的:

NDK开发之引用(局部引用,全局引用,虚全局引用)

我的代码里面大概是这样的:

  jobject collect(JNIEnv *env)
{ for (.., .. , ..)
{ jobject obj = doSomething(env, clazz); //删除引用 env->DeleteLocalRef(obj); env->DeleteLocalRef(clazz); //如果clazz是不变的话, 那么这段代码可以不写, FindClass毕竟还是耗性能的 } } jobject doSomething(JNIEnv *env, jclass clazz)
{ return env->NewObject(clazz, env->NewStringUTF(str.c_str())); }

上面只是我代码的大概结构, 意思懂就行了哈, 这里我其实在主调用函数里面是删除了局部引用的, 但是在调用方法的时候使用了doSomeThing()这样的类似方法, 可以看出,doSomeThing()函数返回的时候生成了一个局部引用jstring, 但是没有释放,这就为后面的崩溃埋下了隐患, 惆怅了一会,好歹让我找到原因了,将doSomething()改成下面的写法后, 问题得到完满的解决,

    jobject doSomething(JNIEnv *env)

    {

             jstring test = env->NewSringUTF(str.c_str());

             jobject ret = env->NewObject(clazz, test);

             env->DeleteLocalRef(test);

       return ret;

     } 

2、 jni引用总结:

引用在 java 程序设计里面扮演了一个很重要的角色, 虚拟机通过追踪引用来管理类实例的生命周期, 虽然虚拟机不能管理 native 代码, 但是 JNI 提供了一系列的方法允许本地代码精确的管理类的引用和生命周期, JNI 支持三种类型的引用, 局部引用 (local references), 全局引用 (global references), 虚全局引用 (weak global references); 嘛, 总之,引用是个很重要的东西, java gc 程序是根据类的引用是否为 0 来决定是否回收类的, 如果本地代码持有 java 对象的引用而不释放的话, 那么 gc 是无法正常回收对象的, 存在内存泄漏的风险。

.局部引用 (Local Reference)

大部分的 JNI 方法返回的都是局部引用, 注意,局部引用不可以缓存下来,局部引用的生命周期局限在本地方法里面, 一旦本地代码返回了,那么本地引用就会被释放掉, 举个栗子: env->FindClass() 返回一个局部引用, 当本地代码返回的时候它将会自动释放掉, 当然本地代码也可以通过 DeleteLocalRef 方法精确的控制局部引用的释放;

jclass clazz;

   clazz = env->FindClass("java/lang/String”);

.全局引用 (Global Reference)

全局引用可以保留有效的引用直到本地方法精确的释放它(比如作为全局变量,局部静态变量用局部引用是不行的,但是全局引用可以), 全局引用可以通过 NewGlobalRef 方法初始化局部引用得到(貌似只有这个方法吧,噗~)

 

 jclass localClazz;

 jclass globalClazz;

      ...

      localClazz = env->FindClass("java/lang/String");

      globalClazz = env->NewGlobalRef(localClazz);

      ...

  env->DeleteLocalRef(localClazz);

辣么问题来了, 就像局部引用可以手动释放一样, 全局引用可以手动释放么? 嘿嘿,一样通过 env->DeleteGlobalRef() 来释放全局引用啦。

.虚全局引用

还有一个全局引用是虚全局引用, 和全局引用的作用一样,但是虚全局引用不会阻止 gc 回收对象, 而全局引用由于持有对象的引用,导致 gc 无法回收。 通过 NewWeakGlobalRef 来新建虚函数引用。

  jclass weakGlobalClazz;

  weakGlobalClazz = env->NewWeakGlobalRef(localClazz); 

喜欢思考的同学这时候会发现一个问题, 由于虚全局引用不能阻止 gc 回收对象, 那么我们肿么知道对象已经被回收了呢,吖,不用担心, jni 已经提供了方法给窝们了

   if (JNI_FALSE == env->IsSameObject(weakGlobalClazz, NULL)){

        //object is still live and can be used.

     }else {

         // object is garbage collected and cannot be used

    }  

删除虚全局引用的方法类似: env->DeleteWeakGlobalRef(weakGlobalClazz); 这个方法任何时候都可以使用。  除非我们精确的释放,全局引用都会保留有效的引用,可以在其他的本地方法里面调用。虚全局引用也一样,不过要注意是否 gc 已经回收了对象了。

注意:

虚拟机支持在多线程下运行本地代码, 我们在开发本地代码的时候需要记住以下几个地方:

1. 局部引用直到本地方法执行期间都是有效的, 局部引用不可以多线程共享, 只有全局引用可以多线程共享。    

2. JNIEnv 接口指针通过被调用的本地方法得到的在相同的线程的其他方法里面都是有效的, 但是它不能够缓存下来给其他线程使用,解决方法也有,通过 JVM 获得 JNIEnv (具体@度娘)。