1 概述
在Java中,通常分为四种引用类型,分别是:强引用、软引用、弱引用以及虚引用。对于一个Java对象来说,当被强引用所引用时,只要该对象可达,就不会被GC回收;当被软引用所引用时,当内存不足时才有可能会被回收;当被弱引用所引用时,该对象随时都可能被GC回收;而当被虚引用所引用时,可以当做没有引用一样,一般用来判断一个对象是否被回收了。
由此可知,引用类型是比较关键的,它决定了一个Java对象如何被GC管理,所占用的内存是否能够被回收,以及何时能够回收。因此,合理的使用引用类型,可以使GC对内存更好的管理,让程序有更好的性能。对于JNI编程,我们也需要合理的使用引用,滥用和乱用引用可能会导致意想不到的错误,而这种错误在JNI中会尤其严重。
在利于JNI进行编程时,对Java对象和数组类型(诸如jobject, jclass, jstring,andjarray)的处理,通常只能借助相关JNI函数来完成,因为JNI的实现只是将它们作为一个不透明的引用暴露给开发者,也就是说实例和数组的内部结构被隐藏了,其内部构成因此不得而知,我们不能直接对其内部结构进行操作,需要通过预先定义的函数来操作该引用来达到目的。
在本地代码中,通常通过JNI提供的相关函数(定义在JNIE结构中),所分配的内存是受JVM的管理的,只要遵循JNI规范来进行编程即可,但是对于通过malloc、new等分配的内存则需要遵循相关语言的特性,自己来管理内存的分配与释放(使用对应的free、delete来释放内存)。
在JNI方法中中,Java的那一套引用机制也是失效的(比如通过赋值来增加对Java对象的引用是行不通的),如果需要告知JVM哪些对象不能被GC回收,则需要借助JNI定义的引用类型来做特殊处理,在JNI中有如下三种引用类型可供使用:
- 局部引用( local references)
- 全局引用(global references)
- 弱全局引用(weak global references)
上面三种JNI引用的作用不同,作用域也不尽一样,也有着不同的生命周期:
- 对于局部引用,在本地方法被调用时创建,在方法返回时(return),该引用将会自动被释放,因此在方法返回之后再使用该引用是不合法的。并不是任意引用都可以使用在所有上下文环境。但在其有效时,将一直阻止所引用的对象被GC回收。
- 对于全局引用和弱全局引用,它们可以在多个方法中使用,在手动释放之前一直有效。但是弱全局引用不会阻止GC回收它所引用的对象,因此使用此引用前判空是必要的。
2 引用详解
2.1 局部引用
- 绝大多数JNI 方法返回的引用是局部引用,它保证了在引用有效的情况下,所引用的对象不会被GC回收。同时也说明,当局部引用不再使用时,仍然引用了对象而不得回收,造成内存消耗。
- 局部引用的作用域或生命周期始于创建它的本地方法,终于本地方法返回。因此,不要尝试使用静态变量来缓存局部引用以达到复用,因为缓存的引用将在本地方法返回时变的无效,因而下次继续使用缓存的引用来操作时,将会触发内存崩溃。
- 通常在局部引用不再使用时,可以显示使用DeleteLocalRef来提前释放它所指向的对象,以便于GC回收。显示的删除局部引用,是为了让它所指的对象能够及时被GC回收,以便节省内存空间,否则只有等到本地方法返回。
- 在一个局部引用被销毁前,可能经过了数个本地方法的传递,最终会在某个方法里决定销毁它或最终被自动销毁。因此在不需要再使用该局部引用时最好释放它,因为它所引用的可能是大对象,占用较多的内存,长此以往可能造成内存吃紧。另外,在所传递的本地方法中创建了局部引用而不释放它,会造成局部引用的数量积累,严重情况可能导致JNI局部引用表的溢出,导致程序崩溃。
- 局部引用是线程相关的,只能在创建它的线程里使用,通过全局变量缓存并使用在其他线程是不合法的。
使用静态变量缓存局部引用的错误使用
JNIEXPORT jstring JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) {
...
// 缓存String的class
static jclass jStringClass;
if (jStringClass == NULL) { // wrong,局部引用在函数返回后会被释放
jStringClass = (*env)->FindClass(env, STRING_PATH);
}
// 获取构造方法的method id ,参数为byte[]
jmethodID jinit = (*env)->GetMethodID(env, jStringClass, "<init>", "([B)V");
int len = strlen(STRING_PATH);
jcharArray jchars = (*env)->NewByteArray(env, len); // 创建一个字节数组
(*env)->SetByteArrayRegion(env, jchars, 0, len, STRING_PATH); // 填充数据
return (*env)->NewObject(env, jStringClass, jinit, jchars); // 创建并返回String
}
上面方法的意图是通过 new String(byte[])来创建一个String对象,并返回给调用者。
第一次调用时jStringClass为NULL,不会有问题,对jStringClass进行初始化,然后使用它。但是第二次调用时静态的jStringClass就不是NULL了,但是它所引用的对象在第一次调用结束后就已经被释放了,因此再进行使用时,则会触发错误,导致应用crash了。此处,可以去掉缓存或者使用全局引用来实现缓存。
2.2 全局引用
- 与局部引用的创建不同,需要使用指定的JNI函数来创建全局引用。通过NewGlobalRef方法来创建全局引用,通过DeleteGlobalRef来释放全局引用。
- 全局引用一直保持有效,直到被程序员手动释放。
- 在失效之前,全局引用确保了所引用的对象不会被GC回收。
- 在失效之前,全局引用可以使用在多次方法调用中,也可以跨方法使用,在多个线程中使用也是合法有效的。
- 可以使用静态变量来缓存全局引用,可以达到复用目的,节省相关性能开销。需要注意的是,全局引用会一直阻止对象被回收,因此合理的释放全局引用是必要的。
如下是对之前代码缓存的改进:
// 缓存string class
static jclass jStringClass = NULL;
if (jStringClass == NULL) {
jclass jlocalClass = (*env)->FindClass(env, STRING_PATH);
if (jlocalClass == NULL)
return NULL;
jStringClass = (*env)->NewGlobalRef(env, jlocalClass); // 创建全局引用
(*env)->DeleteLocalRef(env, jlocalClass); // 删除局部引用
LOGE("Ref", "jStringClass is null.just create it");
} else {
//__android_log_print(ANDROID_LOG_ERROR,"Ref","jStringClass is non-null");
LOGE("Ref", "jStringClass is non-null");
}
使用NewGlobalRef来创建全局引用,并缓存在静态的jStringClass变量中。对于之前使用的局部引用jlocalClass,创建全局引用后它已不再使用,并且它本身也会占用空间,使用DeleteLocalRef来删除它。
2.3 弱全局引用:
- 使用NewWeakGlobalRef来创建,使用DeleteGlobalWeakRef来释放,使引用无效
- 类似全局引用,在无效之前,可以跨方法多次使用,也可以在多线程中使用,无效时使用会导致异常
- 在引用有效的情况下,不保证所引用的对象不被GC回收,因此在使用该引用时,需要做判空处理,使用IsSameObject来检查是否存在
弱全局引用的用法与全局引用类似,不同的是调用的jni函数不同。对之前的示例,我们缓存了jclass类型的jStringClass ,对此我们可能会想,能不能同样对jmethodID类型的jinit进行缓存?先不置可否,看如下修改的代码:
static jmethodID jStringMethodID = NULL;
if (jStringMethodID == NULL) {
jmethodID jlocalId = (*env)->GetMethodID(env, jStringClass, "<init>", "([B)V");
jStringMethodID = (*env)->NewWeakGlobalRef(env,jlocalId); // 不可用于创建全局引用
(*env)->DeleteLocalRef(env,jlocalId);
} else{
if((*env)->IsSameObject(env,jStringMethodID,NULL)==JNI_TRUE) // 是否指向对象被回收?
{
LOGE("Ref", "jStringMethodID 指向的对象被回收了"); // ??
jStringMethodID = (*env)->NewWeakGlobalRef(env,(*env)->GetMethodID(env, jStringClass, "<init>", "([B)V"));
}
}
当jStringMethodID为NULL,就进行查找,然后再使用若全局引用进行包装。第二次再调用的时候,就可以直接使用之前缓存好的值。IsSameObject用来判断指向的对象是否为同一个,稍后介绍。
这里我们使用弱全局引用来实现,貌似是OK的,但是运行后就会发现应用crash了。为什么会造成这种情况的发生呢?查看jni.h这个头文件,我们很容易的找到jmethodID的定义:
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID* jmethodID; /* method IDs */
可以发现jmethodID实际上是一个结构体指针类型,指向_jmethodID。而NewWeakGlobalRef(JNIEnv*, jobject)需要传递的参数是jobject,实际上是一个void指针类型,因此不可以对jmethodID类型变量进行JNI的引用操作,因为jmethodID所指向的实体推测应该是一个C风格的结构,而非一个Java对象。对于jfieldID也有同样的道理。
typedef void* jobject;
假如我们需要缓存jmethodID、jfieldID,直接使用静态变量来缓存即可。比如如下使用:
static jmethodID jStringMethodID = NULL;
if (jStringMethodID == NULL) {
jStringMethodID = (*env)->GetMethodID(env, jStringClass, "<init>", "([B)V");
} else {
LOGE("Ref", "jStringMethodID is non-null");
}
对于经常会被使用的变量,我们可以在一开始就将它们缓存起来。添加如下native方法:
public native static void nativeInit();
从名字看,是需要让本地代码进行初始化,为了达到在任何函数之前调用,我们可以在load库之后就调用它,如下:
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
nativeInit(); // 调用
}
在此native方法的实现里,我们会进行一些初始化功能,如下示例(仅做示例):
jmethodID g_string_init_mid;
jobject g_string_class;
JNIEXPORT void JNICALL
Java_com_pecuyu_jnirefdemo_MainActivity_nativeInit(JNIEnv *env, jclass type) {
// init
g_string_class = (*env)->NewGlobalRef(env, (*env)->FindClass(env, STRING_PATH));
g_string_init_mid = (*env)->GetMethodID(env, g_string_class, "<init>", "([B)V");
if(g_string_class==NULL||g_string_init_mid==NULL)
{
exit(-1); // 退出
}
}
上面我们缓存了string的class对象以及构造方法String(byte[])的methodId。之后我们调用其他JNI函数,就可以直接使用这两个变量了。如下示例:
JNIEXPORT jstring JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,
jstring jmsg) {
if (jmsg != NULL) {
jboolean isCopy; // 传出参数
const char *msg = (*env)->GetStringUTFChars(env, jmsg, &isCopy);
if (isCopy) // 是否为拷贝,由函数内部决定
{
LOGE("Ref", "copy from the origin jmsg");
}
LOGE("Ref", "%s", msg);
(*env)->ReleaseStringUTFChars(env, jmsg, msg); // 释放字符串占用内存
}
int len = strlen(STRING_PATH);
jcharArray jchars = (*env)->NewByteArray(env, len); // 创建一个字节数组
(*env)->SetByteArrayRegion(env, jchars, 0, len, STRING_PATH);
return (*env)->NewObject(env, g_string_class, g_string_init_mid, jchars); // 创建并返回String
}
最后,需要特别注意,全局变量缓存的全局引用,需要在全局引用不再使用时进行释放,否则被其所引用的对象得不到回收,而造成了内存泄露。添加如下方法,用来释放全局引用:
public native static void delRefs();
// implementation
JNIEXPORT void JNICALL
Java_com_pecuyu_jnirefdemo_MainActivity_delRefs(JNIEnv *env, jclass type) {
// free global refs
(*env)->DeleteGlobalRef(env,g_string_class);
//...
}
2.4 引用比较
使用IsSameObject比较上面三种引用的任意两个,可以比较是否指向了同一个对象。
- 函数原型如下:
jboolean (IsSameObject)(JNIEnv, jobject, jobject);
方法返回:
- JNI_TRUE(or 1) 引用同一个对象
- JNI_FALSE(or0) 引用非同一个对象
JNI中的NULL指向了JVM的null,与NULL作比较实际上是判断对象引用是否为null,使用如下:
(*env)->IsSameObject(env, obj, NULL)
或
obj == NULL
对于一个非NULL的弱全局引用,我们可以通过与NULL的比较,来判断它所引用的对象是否还存活,如下代码:
(*env)->IsSameObject(env, wobj, NULL)
- 返回JNI_TRUE表示弱全局引用wobj指向了null,即它所引用的对象已经被JVM回收
- 返回 JNI_FALSE 表示它所引用的对象还存活
3 释放引用
三点概要:
内存占用:
每个正在使用的JNI引用,不仅被它所引用的对象会占据内存,它自身也会占一定的内存空间,因此适时释放JNI引用是很必要的。
局部引用数量:
我们需要关注在特定时间下创建的局部引用的数量,因为在程序执行的某个时间点上,所能创建的局部引用的数量是有上限的,即过多的数量可能会导致JNI内部的局部引用表溢出。
内存溢出风险
虽然局部引用可以不经处理,在本地方法返回时最终仍然会自动被虚拟机释放,但是在过多的局部引用的情况下,可能导致很多对象得不到释放(尤其是有大对象被引用时),而这可能导致应用内存耗尽,即可能出现内存溢出的风险。
3.1 释放局部引用
- 在某个本地方法调用中,创建的局部引用的数量不宜过多,否则可能导致JNI内部的局部引用表溢出,应该适时释放不再使用的局部引用。尽管在本地方法返回后,局部引用会被自动释放,但是在这之前若不主动释放,局部引用所引用的对象会一直存活,可能导致使用的内存在方法调用时居高不下,甚至可能出现OOM的风险。
- 当一个本地方法不再返回,也就是内部无限循环执行,此时适时释放无用局部引用是必须的。例如,一个本地方法可能进入一个无终止的事件派遣循环。释放在循环中创建的局部引用至关重要(crucial),因此不会让引用数量累增而导致内存泄露
- 局部引用引用了一个较大的对象。在本地方法调用时,可能会访问java中大对象,局部引用在被释放之前会一直阻止JVM回收这个大对象,即是之后不再使用该对象,造成了内存浪费,因此适时释放该局部引用是合理的选择。
- 使用DeleteLocalRef 来使用局部引用
下面的例子是在循环中不断创建局部引用,但是不主动释放它们,看看会有什么结果:
JNIEXPORT void JNICALL
Java_com_pecuyu_jnirefdemo_MainActivity_refsOverflow(JNIEnv *env, jobject instance) {
jstring jstr;
char buf[60];
for (int i = 0; i < 1000; ++i) {
sprintf(buf,"new string %d",i);
jstr=(*env)->NewStringUTF(env,buf); // 创建一个java string
}
}
使用for循环来不停的创建string对象,却没有主动释放。要知道,和局部变量不同的是,局部引用的重新赋值,并不会改变已经创建了的局部引用的指向,除非手动删除,否则会一直积累,直到JNI函数完全返回,所有的局部引用才会都被释放。
因此本例的局部引用的数量会一直积累,但因为超过了局部引用表容量的最大值,导致了错误发生,如下输出:
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] JNI ERROR (app bug): local reference table overflow (max=512)
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] local reference table dump:
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] Last 10 entries (of 512):
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] 511: 0x12dae938 java.lang.String "new string : 505"
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] 510: 0x12dae900 java.lang.String "new string : 504"
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] 509: 0x12dae8c8 java.lang.String "new string : 503"
...
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] 502: 0x12dae740 java.lang.String "new string : 496"
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] Summary:
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] 509 of java.lang.String (509 unique instances)
com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] 2 of java.lang.Class (2 unique instances)
/com.pecuyu.jnirefdemo A/art: art/runtime/runtime.cc:427] 1 of java.lang.String[] (3 elements)
从错误可以看出,是因为local reference table overflow ,也就是局部引用表溢出了,最大值是512,而这里曾经的很明显超了。 从下面的摘要Summary中,可以看到具体的使用情况。要想解决问题,也就一行代码,及时的删除局部引用:
...
for (int i = 0; i < 1000; ++i) {
sprintf(buf,"new string : %d",i); // 格式化字串
jstr=(*env)->NewStringUTF(env,buf); // 创建一个java string
(*env)->DeleteLocalRef(env,jstr); // 删除局部引用
}
...
下面的一个例子是引用大对象,当不再使用时,就马上释放它,让内存得到释放(来自官方示例):
/* A native method implementation */
JNIEXPORT void JNICALL
Java_pkg_Cls_func(JNIEnv *env, jobject this)
{
lref = ... /* 引用一个占大内存的Java对象 */
... /* 最后使用引用lref */
(*env)->DeleteLocalRef(env, lref); // 释放局部引用
lengthyComputation(); /* 其他一些耗时的操作 */
return; /* all local refs are freed */
}
3.2 Java2的新特性
Java2开始提供了一套新的方法来管理局部引用的生命周期,包括 EnsureLocalCapacity, NewLocalRef, PushLocalFrame, 以及 PopLocalFrame.
JNI规范规定,JVM自动保证每个本地方法都至少能创建16个局部引用(实际支持的数量可能大得多)。通常对于和JVM的对象没有复杂交互的本地方法来说,这个数量是足够的,但是如果需要创建额外的局部引用,我们可以调用EnsureLocalCapacity方法来确保一定数量的局部引用的空间是可用的。使用Push/PopLocalFrame来允许程序员创建一个局部引用的嵌套作用域,如下一段官方示例:
* The number of local references to be created is equal to
the length of the array. */
if ((*env)->EnsureLocalCapacity(env, len)) < 0) { // 确保是否有len个局部引用的空间
... /* out of memory */
}
#define N_REFS ... /* the maximum number of local references
used in each iteration */
for (i = 0; i < len; i++) {
if ((*env)->PushLocalFrame(env, N_REFS) < 0) { // push
... /* out of memory */
}
jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
(*env)->PopLocalFrame(env, NULL); // pop
}
PushLocalFrame为给定数量的局部引用创建了一个新的作用域.PopLocalFrame销毁了之前创建的作用域,并释放了在此作用域的局部引用(两个函数之间创建的局部引用)。使用这两个函数的好处是显而易见的,它们动态创建了一个嵌套的作用域,可以很方便的管理其中的局部变量的生命周期,而不必管其中创建的任意一个引用,因此在PopLocalFrame被调用后,该作用域被销毁,而其中的所有局部引用将会被释放。
在本地代码中,可能创建超过16个引用,或者在Push/PopLocalFrame之间的作用域里,可能创建了超过之前所能保证的容量,JVM的实现会尽可能为局部引用分配内存,但是不能保证一定有可用内存。当无法分配足够内存时,虚拟机会退出,造成应用崩溃。因此需要及时的释放局部引用,以保证局部变量有足够的内存,避免出错。
接下来看一个Android源码中相关的例子,AndroidRuntime在启动后,调用这个函数是用来注册一些与VM相关的常用JNI函数,将Java层与native层连接起来:
/*
* Register android native functions with the VM.
*/
/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
ATRACE_NAME("RegisterAndroidNatives");
/*
* This hook causes all future threads created in this process to be
* attached to the JavaVM. (This needs to go away in favor of JNI
* Attach calls.)
*/
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
ALOGV("--- registering native functions ---\n");
/*
* Every "register" function calls one or more things that return
* a local reference (e.g. FindClass). Because we haven't really
* started the VM yet, they're all getting stored in the base frame
* and never released. Use Push/Pop to manage the storage.
*/
env->PushLocalFrame(200); // push 方法
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
env->PopLocalFrame(NULL); // 注册失败也需要pop
return -1;
}
env->PopLocalFrame(NULL); // pop方法
//createJavaThread("fubar", quickTest, (void*) "hello");
return 0;
}
接下来,通过实例来测试一下Push/PopLocalFrame的作用,下面的测试是循环1000次,每次都在内部循环创建100个string,若不使用Push/PopLocalFrame,则肯定是直接局部引用表溢出了,但当使用Push/PopLocalFrame时,程序表现的很正常。可以知道,每次内循环创建的100个局部引用,在pop方法调用后,就被释放掉了:
JNIEXPORT void JNICALL
Java_com_pecuyu_jnirefdemo_MainActivity_pushPopRefs(JNIEnv *env, jobject instance) {
jstring jstr;
char buf[60];
for (int i = 0; i < 1000; ++i) {
(*env)->PushLocalFrame(env, 100); // push
for (int j = 0; j < 100; ++j) {
sprintf(buf, "new string : %d", j);
jstr = (*env)->NewStringUTF(env, buf); // 创建一个java string
}
(*env)->PopLocalFrame(env, NULL); // pop
}
}
NewLocalRef函数可以用来创建一个局部引用,参数可以是局部引用,全局引用,以及弱全局引用。
Java2支持命令行选项-verbose:jni。当此选项被开启,当局部引用的数量超过预设容量时,JVM将报告此问题。
3.3 释放全局引用
- 在本地代码中,当一个全局引用不再使用时,调用DeleteGlobalRef函数来释放它,从而被它所引用的对象能够被GC回收。假如没有调用该函数来删除此全局引用(或调用函数失败),那么它所引用的对象将一直不会被GC回收,即使所引用的对象已经不再使用了,这将导致潜在的内存泄露,长此以往则可能导致应用的性能问题甚至崩溃。
- 在本地代码中,当一个弱全局引用不再使用时,调用DeleteWeakGlobalRef函数来释放它。即使不手动释放此引用(或调用函数失败),GC也可能回收了它所引用的对象,但是它本身所占用的内存却得不到回收。因此在使用该引用时,需要做判空处理(使用IsSameObject)。
释放全局引用的例子如下:
JNIEXPORT void JNICALL
Java_com_pecuyu_jnirefdemo_MainActivity_delRefs(JNIEnv *env, jclass type) {
// free global refs
(*env)->DeleteGlobalRef(env,g_string_class); // 对于弱全局引用,需要调用DeleteWeakGlobalRef
//...
}
4 管理JNI引用的原则
一般有两种风格本地代码需要注意:
- 直接实现native方法的函数(JNI函数)
- 使用在任意上下文的工具函数
4.1 直接实现native方法的函数:
- 1、避免在循环中创建了过多局部引用,而没有及时释放,以免造成局部引用表溢出。在有循环或大量创建局部引用的地方,可以使用Push/PopLocalFrame来管理这些局部引用,不必为创建的引用没有释放而担心。
- 2、在本地方法返回前,应该尽可能的主动释放不再使用的引用,即使创建的局部引用的数量没有超过局部引用表的大小。
- 3、避免在不返回的函数中创建了引用,但没有及时释放无用的引用
- 4、native方法的多次调用,不应该导致全局或弱全局引用的累加,因为在调用返回后它们是不会被自动释放的。
4.2 本地工具函数:
- 1、在整个函数的任意执行路径上,都不能泄露局部引用(因为工具函数可能会在任意上下文环境被重复调用,因此多余的引用可能会导致内存溢出)
- 2、如果工具函数返回原始类型,不应该有积累局部、全局以及弱全部引用的副作用
- 3、如果工具函数返回引用类型,除了返回值引用外,不应该有积累其他局部、全局以及弱全部引用的副作用
- 4、在工具函数里面是可以创建全局、弱全局引用的,因为它们只会在最开始的一次调用才会创建,其他调用应该确保不会重复创建
- 5、确保工具函数的返回值类型是一定的,不能一会返回局部引用,一会又返回全局引用,因为调用者需要明确返回的引用类型,以方便使用哪个JNI函数来释放返回的引用
举个栗子:
jstring newStringWithMsg(JNIEnv *env, const char *msg) {
int len = strlen(msg);
jcharArray jchars = (*env)->NewByteArray(env, len); // 创建一个字节数组
(*env)->SetByteArrayRegion(env, jchars, 0, len, msg);
jstring jstr = (*env)->NewObject(env, g_string_class, g_string_init_mid, jchars); // 创建string对象
(*env)->DeleteLocalRef(env, jchars); // 删除局部引用
return jstr;
}
对于上面的工具函数,删除jchars局部引用是必要的,否则当该函数被频繁调用时,可能导致局部引用表的溢出。
4.3 Push/PopLocalFrame管理局部变量的生命周期
jint PushLocalFrame(JNIEnv *env, jint capacity);
jobject PopLocalFrame(JNIEnv *env, jobject result);
- 1、两个函数成对使用,调用了push方法而没有调用pop方法,可能导致不可预知的错误。换句话说,对于函数返回的任意一条路径上,都必须对应的调用pop方法,不能存在一条执行路径上调用了push但没有调用pop的情况。
- 2、在两个函数之间创建的局部引用的生命周期被限制在这之间,pop函数调用后,会释放这之间创建的所有引用
- 3、PushLocalFrame函数返回0表示成功,否则返回负数,并抛出OutOfMemoryError
- 4、使用PopLocalFrame时,可以传递一个之前使用的局部引用做参数,以保证它不会被释放,通过返回值获取该引用
针对最后一点,来举个栗子:
...
jstring jstr = NULL;
jstring result = NULL;
char buf[60];
if ((*env)->PushLocalFrame(env, 100) < 0) // push
{
return; // OutOfMemoryError
}
for (int j = 0; j < 100; ++j) {
sprintf(buf, "new string : %d", j);
jstr = (*env)->NewStringUTF(env, buf); // 创建一个java string
}
// 保留最后一条jstr引用,不会被释放,通过返回值返回该引用
result = (*env)->PopLocalFrame(env, jstr); // pop
if (result != NULL) {
const char *chs = (*env)->GetStringUTFChars(env, result, NULL);
LOGE("refs", "chs = %s", chs);
(*env)->DeleteLocalRef(env, result);
}
...
4.4 小结
总结一下各种引用的特点:
- | 局部引用 | 全局引用 | 弱全局引用 |
---|---|---|---|
是否跨方法 | - | 支持 | 支持 |
是否跨线程 | - | 支持 | 支持 |
引用释放 | native方法返回或调用DeleteLocalRef | 调用DeleteGlobalRef | 调用DeleteWeakGlobalRef |
引用对象回收时机 | 引用释放后 | 引用释放后 | 不阻止对象回收 |
缓存 | - | 支持 | 支持 |
参考:The Java Native Interface Programmer’s Guide and Specification