JNI 局部引用和全局引用

1,436 阅读7分钟

Java 和 JNI 之间,传递基本类型对象的方式是值复制,而传递引用类型对象的方式是通过引用。在 JNI 中,有二种引用,局部引用(Local Reference)、全局引用(Global Reference)。弱全局引用(Weak Global Reference)是全局引用的一种特殊形式。

局部引用

局部引用只有在JNI层的函数调用期期间有效,函数返回后,局部引用会被自动释放。局部引用会阻止垃圾收集器回收底层对象,只有当函数返回或者手动释放局部引用,垃圾收集器才有可能回收底层对象。

虽然说局部引用会被自动释放,但是我们最好还是手动释放,并且在某些极端情况下,为了不造成内存紧张,还是需要手动释放。例如

  1. 在JNI层的方法中,引用了一个占用很大内存的Java对象,然后对这个对象执行一些操作,然后再执行一些与这个对象无关的操作,最后返回。这个Java对象在JNI层被引用期间,是无法被垃圾回收的,那么当我们执行完了与这个对象相关的操作后,最好立即释放这个局部引用,然后再执行一些与这个对象无关的操作,最后函数返回。
  2. 当在JNI函数中,通过循环创建大量的局对象时,最好及时释放局部引用,否则可能造成OOM。

那么如何释放局部引用呢?可以通过如下函数

void DeleteLocalRef(JNIEnv *env, jobject localRef);

我在 Android 源码中找到一个释放局部引用的例子

    virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia)
    {
        // 创建一个局部引用
        jstring pathStr;
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
            mEnv->ExceptionClear();
            return NO_MEMORY;
        }
		
        // 使用刚创建的局部引用作为参数,调用Java对象的方法
        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                fileSize, isDirectory, noMedia);
        
        // 局部引用已经不再需要,立即释放它
        mEnv->DeleteLocalRef(pathStr);
        return checkAndClearExceptionFromCallback(mEnv, "scanFile");
    }

从这个源码例子可以看出,尽管不手动释放这个局部引用也没啥影响,但是源码中还是通过DeleteLocalRef()手动释放。源码都尚且如此,何况我们一个普通的开发者呢?所以,在JNI函数中创建了局部引用,使用完毕后,手动释放这个局部引用。

其实局部引用还可以通过如下函数手动创建

jobject NewLocalRef(JNIEnv *env, jobject ref);

参数ref可以是全局引用、弱全局引用或局部引用。目前我还不清楚为全局引用、局部引用创建局部引用有什么用,但是为弱全局引用创建局部引用还是有用的,这个作用会在文章后面介绍。

全局引用

NI函数在返回后,局部引用终究是会被释放。然而有时候我们需要把Java层传下来的对象进行保存,然后在函数返回后的某个时刻再进行操作。此时就需要使用如下函数为这个对象创建一个全局引用。

jobject NewGlobalRef(JNIEnv *env, jobject obj);

参数 obj 可以是一个局部引用,也可以是一个全局引用。这个函数执行成功会返回一个引用,然而如果发生OOM,返回NULL。

全局引用在不需要的时候,必须手动释放,使用的函数原型如下

void DeleteGlobalRef(JNIEnv *env, jobject globalRef);

Android 源码中有如下一个例子

    MyMediaScannerClient(JNIEnv *env, jobject client)
        :   mEnv(env),
            mClient(env->NewGlobalRef(client)), // 创建全局引用 
    {}
    
    virtual ~MyMediaScannerClient()
    {
        // 释放全局引用
        mEnv->DeleteGlobalRef(mClient);
    }

MyMediaScannerClient 的构造函数的参数 client 是一个Java层的对象,它通过 NewGlobalRef() 函数创建一个全局引用,并用 mClient 变量保存。有了这个全局引用,就可以阻止垃圾收集器对它进行回收,这样就可以随时安全操作这个Java对象。

当 MyMediaScannerClient 销毁时,会调用它的析构函数,此时就调用 DeleteGlobalRef() 来删除这个全局引用。这个全局引用被释放后,JNI层就不再阻止这个对象的垃圾回收。

弱全局引用

弱全局引用(Weak Global References)是一种特殊的全局引用,当一个底层Java对象只被弱全局引用所指向,这个弱全局引用不会阻止垃圾收集器回收这个底层Java对象。

我们可以通过如下函数来创建弱全局引用

jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);

当参数obj为NULL或者发生OOM(虚拟机会抛出OOM异常)时,函数返回NULL。

弱全局引用的目的很显示,那就是不阻止垃圾回收,一般是为了防止内存泄露。

弱全局引用也需要一定的虚拟机资源,因此在不需要弱全局引用时,需要使用如下函数释放

void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

既然弱全局引用不会阻止垃圾回收,那么它指向的底层对象可能被释放,因此在使用它时,我们最好使用下面的函数来把弱全局引用与NULL进行比较

// 测试两个引用是否指向相同的Java对象
// 如果两个引用指向相同的Java对象,或者两个引用都为NULL,
// 函数返回JNI_TRUE,否则返回JNI_FALSE
jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);

你可能还没有发现,弱全局引用的类型为jweak,那么如何使用它呢?其实我们只要搞清楚jweak和jobject的关系就行,代码如下

class _jobject {};

typedef _jobject*       jobject;
typedef _jobject*       jweak;

原来,jobject和jweak都是指针,而且都指向同一种类型的对象,因此把jweak当作jobject来用就行了。

我们来看看Android源码中如何使用这个弱全局引用的

JMediaCodec::JMediaCodec(
        JNIEnv *env, jobject thiz,
        const char *name, bool nameIsType, bool encoder)
{
    // ...
    // 创建弱全局引用
    mObject = env->NewWeakGlobalRef(thiz);
}


JMediaCodec::~JMediaCodec() {
    // ...

    // 析构函数中,删除弱全局引用
    env->DeleteWeakGlobalRef(mObject);
}


void JMediaCodec::handleFrameRenderedNotification(const sp<AMessage> &msg) {
    // ...

    // mOjetct是一个弱全局引用
    // 调用mObject的Java方法
    env->CallVoidMethod(
            mObject, gFields.postEventFromNativeID,
            EVENT_FRAME_RENDERED, arg1, arg2, obj);
}

从这个例子可以看出,虽然mObject类型虽然为jweak,但是它调用Java方法的方式与jobject并没有区别。

另外,源码中并没有如下代码来检测这个弱全局指向的Java对象是否被释放了

jboolean b = env->IsSameObject(mObject, NULL);

最后我们来讨论一个把弱全局引用转化为局部引用的情况。由于弱全局引用无法阻止垃圾回收,为了在JNI函数中安全使用这个弱全局引用,可以把它转化为局部引用,这样就能在函数返回前,暂时阻止垃圾回收,于是可以安全操作底层的Java对象。

Android源码的一个例子如下

virtual void onPositionLost(RenderNode& node, const TreeInfo* info) override {

    // 为弱全局引用创建一个局部引用,在这个函数返回前,
    // 可以防止底层Java对象被垃圾回收
    jobject localref = env->NewLocalRef(mWeakRef);
            
    // ...
    
    // 局部引用阻止了垃圾回收,因此可以安全调用Java对象的方法
    env->CallVoidMethod(localref, gSurfaceViewPositionLostMethod,
                    info ? info->canvasContext.getFrameNumber() : 0);
    env->DeleteLocalRef(localref);
}

那么有人可能会问,如果在为弱全局引用创建局部引用时,底层Java对象已经回收了,那怎么办呢?这个只能你用代码来保证,你要保证在Java层对象销毁前,释放底层对象,底层对象再释放弱全局引用。

工作感想

在我的工作经验中,几乎没有使用弱全局引用,用的最多的是全局引用。不过如果我们遇到源码中使用弱全局引用的情况,应该多思考为何这样使用,以便在后面的工作使用好弱全局引用。

参考

docs.oracle.com/javase/7/do…

docs.oracle.com/javase/7/do…