在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中有如下三种引用类型可供使用:
上面三种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, "" , "([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了。此处,可以去掉缓存或者使用全局引用来实现缓存。
如下是对之前代码缓存的改进:
// 缓存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来删除它。
弱全局引用的用法与全局引用类似,不同的是调用的jni函数不同。对之前的示例,我们缓存了jclass类型的jStringClass ,对此我们可能会想,能不能同样对jmethodID类型的jinit进行缓存?先不置可否,看如下修改的代码:
static jmethodID jStringMethodID = NULL;
if (jStringMethodID == NULL) {
jmethodID jlocalId = (*env)->GetMethodID(env, jStringClass, "" , "([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, "" , "([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, "" , "([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, "" , "([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);
//...
}
使用IsSameObject比较上面三种引用的任意两个,可以比较是否指向了同一个对象。
jboolean (IsSameObject)(JNIEnv, jobject, jobject);
方法返回:
JNI中的NULL指向了JVM的null,与NULL作比较实际上是判断对象引用是否为null,使用如下:
(*env)->IsSameObject(env, obj, NULL)
或
obj == NULL
对于一个非NULL的弱全局引用,我们可以通过与NULL的比较,来判断它所引用的对象是否还存活,如下代码:
(*env)->IsSameObject(env, wobj, NULL)
三点概要:
内存占用:
每个正在使用的JNI引用,不仅被它所引用的对象会占据内存,它自身也会占一定的内存空间,因此适时释放JNI引用是很必要的。
局部引用数量:
我们需要关注在特定时间下创建的局部引用的数量,因为在程序执行的某个时间点上,所能创建的局部引用的数量是有上限的,即过多的数量可能会导致JNI内部的局部引用表溢出。
内存溢出风险
虽然局部引用可以不经处理,在本地方法返回时最终仍然会自动被虚拟机释放,但是在过多的局部引用的情况下,可能导致很多对象得不到释放(尤其是有大对象被引用时),而这可能导致应用内存耗尽,即可能出现内存溢出的风险。
下面的例子是在循环中不断创建局部引用,但是不主动释放它们,看看会有什么结果:
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 */
}
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将报告此问题。
释放全局引用的例子如下:
JNIEXPORT void JNICALL
Java_com_pecuyu_jnirefdemo_MainActivity_delRefs(JNIEnv *env, jclass type) {
// free global refs
(*env)->DeleteGlobalRef(env,g_string_class); // 对于弱全局引用,需要调用DeleteWeakGlobalRef
//...
}
一般有两种风格本地代码需要注意:
举个栗子:
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局部引用是必要的,否则当该函数被频繁调用时,可能导致局部引用表的溢出。
jint PushLocalFrame(JNIEnv *env, jint capacity);
jobject PopLocalFrame(JNIEnv *env, jobject result);
针对最后一点,来举个栗子:
...
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);
}
...
总结一下各种引用的特点:
- | 局部引用 | 全局引用 | 弱全局引用 |
---|---|---|---|
是否跨方法 | - | 支持 | 支持 |
是否跨线程 | - | 支持 | 支持 |
引用释放 | native方法返回或调用DeleteLocalRef | 调用DeleteGlobalRef | 调用DeleteWeakGlobalRef |
引用对象回收时机 | 引用释放后 | 引用释放后 | 不阻止对象回收 |
缓存 | - | 支持 | 支持 |
拓展阅读:JNI笔记 : 数据类型、JNI函数与签名
参考:The Java Native Interface Programmer’s Guide and Specification