前言

平时写 Java 代码对创建的对象,我们很少会去关心怎么手动释放内存,大多数时候都有 GC 去帮我们回收。

然而在 JNI 中,几乎都是 C/C++ 的代码,它们是没有 GC 的,所以对内存的使用就需要格外小心了。

因此在 JNI 中提供了三种引用类型,分别是局部引用、全局引用、弱全局引用。下面介绍这三种引用。

局部引用

我们平时和局部引用打交道是最多的,比如我们经常在函数中使用的 NewStringUTF("hello,world") ,这就是一个局部引用。

局部引用只有在函数的作用域内才有效,函数执行完成之后会自动释放,所以即便没有手动释放也没有关系。

但是创建过多局部引用就有可能导致内存溢出或引用表溢出,这种情况大多出现在有循环的代码中。所以最好我们在使用完之后主动调用 DeleteLocalRef 来手动释放。

PushLocalFrame & PopLocalFrame

有时候我们需要创建许多对象,但是一个一个的手动释放又非常麻烦,为了解决这个问题 JNI 提供了 PushLocalFramePopLocalFrame 来解决这个问题。

它们是配套使用的,需要成对出现。如下所示

1
2
3
4
5
6
7
8
void TestLocalFrame(JNIEnv *env) {
    env->PushLocalFrame(10);
    jstring jstr1 = env->NewStringUTF("hello1");
    jstring jstr2 = env->NewStringUTF("hello2");
    jstring jstr3 = env->NewStringUTF("hello3");
    ...
    env->PopLocalFrame(NULL);
}

PushLocalFrame 之后的代码会在遇到 PopLocalFrame 后一次性全部释放,这样我们就不用手动一个一个的去释放了。

上面这种情况是没有返回值的,如果有返回局部栈帧中的局部引用应该怎么办?

JNI 提供的 PopLocalFrame 其实是有一个参数的, jobject PopLocalFrame(jobject result); 我们把需要保留的局部引用放到 result 中就可以了,上面的例子中我们传入 NULL 表示不需要返回到之前的栈帧中。

1
2
3
4
5
6
7
8
jstring TestLocalFrame(JNIEnv *env) {
    env->PushLocalFrame(10);
    jstring jstr1 = env->NewStringUTF("hello1");
    jstring jstr2 = env->NewStringUTF("hello2");
    jstring jstr3 = env->NewStringUTF("hello3");
    ...
    return env->PopLocalFrame(jstr1);
}

这样 jstr1 就会在父帧中被重新创建,所以其实 jstr1 还是会失效。只不过重新创建了一个新的引用。

EnsureLocalCapacity

JNI 规范中指出, JVM 要确保每个 Native 方法至少可以创建 16 个局部引用,所以我们平时开发一般是够用的,但是就是会碰到极个别情况下超过 16 的,这时候就需要使用 EnsureLocalCapacity 来确保是否能够创建足够的局部引用。

1
2
3
4
5
if (env->EnsureLocalCapacity(50) ==  0) {
    for (int i = 0; i < len;  ++i) {
        jstring  jstr = env->NewStringUTF("hello, world");
    }
}

EnsureLocalCapacity 返回 0 表示创建成功。

全局引用

上一节我们说道局部引用在函数退出后会被释放,并且局部变量不能跨函数,线程使用。

但我们有时候需要把局部变量保存起来,以便在其它方法或线程中使用。这时候我们会想到使用静态变量来进行赋值,但是这样做法是有问题的。

因为即便你赋值给静态变量,它本质上还是局部引用,函数结束后还是会被释放,所以在函数结束后静态变量其实指向了一个非法地址。

为了解决这个问题, JNI 提供了 NewGlobalRef 把局部引用转为全局引用。

1
2
3
4
5
6
7
jobject g_callback;

extern "C" JNIEXPORT void JNICALL
AddListener(JNIEnv *env, jobject thiz, jobject callback) {
    // ...
    g_listener = env->NewGlobalRef(callback);
}

这里需要注意判断两个 Java 对象是不是同一个,需要用 IsSameObject ,而不是用 == 判断。

1
2
3
4
5
if (env->IsSameObject(g_callback, callback) != JNI_FALSE) {
    // 对象是同一个
} else {
    // 对象不是同一个
}

在不使用的时候记得要使用 env->DeleteGlobalRef(callback) 释放。

弱全局引用

弱全局引用和全局引用一样也是全局引用,但是弱全局引用并不会阻止对象被回收。

弱全局引用使用 env->NewWeakGlobalRef(callback) 进行初始化,使用 env->DeleteWeakGlobalRef 来删除全局弱引用。

1
2
3
4
5
if (env->IsSameObject(g_weakCallback, NULL) != JNI_FALSE) {
    // 对象未被释放
} else {
    // 对象已经被释放了
}

参考