三种引用的简介及区别
局部引用
局部引用:通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放。(*env)->DeleteLocalRef(env,local_ref)
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
jcharArray charArr = (*env)->NewCharArray(env, len);
jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj); // 通过NewLocalRef函数创建
...
全局引用
全局引用:调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef手动释放(*env)->DeleteGlobalRef(env,g_cls_string);
static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
g_cls_string = (*env)->NewGlobalRef(env,cls_string);
}
弱全局引用
弱全局引用:调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef手动释放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)
static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);
}
引用管理
引用缓存
1. 为什么要缓存引用?
当我们在本地代码中要访问Java对象的字段或调用它们的方法时,本机代码必须调用FindClass()、GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID()。对于 GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID(),为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。
2. 缓存引用的两种方式
2.1 使用时缓存
// AccessCache.c
#include "com_study_jnilearn_AccessCache.h"
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField
(JNIEnv *env, jobject obj)
{
// 第一次访问时将字段存到内存数据区,直到程序结束才会释放,可以起到缓存的作用
static jfieldID fid_str = NULL;
jclass cls_AccessCache;
jstring j_str;
const char *c_str;
cls_AccessCache = (*env)->GetObjectClass(env, obj); // 获取该对象的Class引用
if (cls_AccessCache == NULL) {
return;
}
// 先判断字段ID之前是否已经缓存过,如果已经缓存过则不进行查找
if (fid_str == NULL) {
fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;");
// 再次判断是否找到该类的str字段
if (fid_str == NULL) {
return;
}
}
j_str = (*env)->GetObjectField(env, obj, fid_str); // 获取字段的值
c_str = (*env)->GetStringUTFChars(env, j_str, NULL);
if (c_str == NULL) {
return; // 内存不够
}
printf("In C:\n str = \"%s\"\n", c_str);
(*env)->ReleaseStringUTFChars(env, j_str, c_str); // 释放从从JVM新分配字符串的内存空间
// 修改字段的值
j_str = (*env)->NewStringUTF(env, "12345");
if (j_str == NULL) {
return;
}
(*env)->SetObjectField(env, obj, fid_str, j_str);
// 释放本地引用
(*env)->DeleteLocalRef(env,cls_AccessCache);
(*env)->DeleteLocalRef(env,j_str);
}
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
jcharArray elemArray;
jchar *chars = NULL;
jstring j_str = NULL;
static jclass cls_string = NULL;
static jmethodID cid_string = NULL;
// 注意:这里缓存局引用的做法是错误,这里做为一个反面教材提醒大家,下面会说到。
if (cls_string == NULL) {
cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
}
// 缓存String的构造方法ID
if (cid_string == NULL) {
cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
if (cid_string == NULL) {
return NULL;
}
}
printf("In C array Len: %d\n", len);
// 创建一个字符数组
elemArray = (*env)->NewCharArray(env, len);
if (elemArray == NULL) {
return NULL;
}
// 获取数组的指针引用,注意:不能直接将jcharArray作为SetCharArrayRegion函数最后一个参数
chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL);
if (chars == NULL) {
return NULL;
}
// 将Java字符数组中的内容复制指定长度到新的字符数组中
(*env)->SetCharArrayRegion(env, elemArray, 0, len, chars);
// 调用String对象的构造方法,创建一个指定字符数组为内容的String对象
j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray);
// 释放本地引用
(*env)->DeleteLocalRef(env, elemArray);
return j_str;
}
关键在于利用了C中static关键字在第一次访问时将字段存到内存数据区,直到程序结束才会释放,可以起到缓存的作用。
注意:cls_string是一个局部引用,与方法和字段ID不一样,局部引用在函数结束后会被VM自动释放掉,这时cls_string成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_com_xxxx_newString这个函数的时候,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的。
2.2 类静态初始化缓存
package com.study.jnilearn;
public class AccessCache {
public static native void initIDs();
public native void nativeMethod();
public void callback() {
System.out.println("AccessCache.callback invoked!");
}
public static void main(String[] args) {
AccessCache accessCache = new AccessCache();
accessCache.nativeMethod();
}
static {
System.loadLibrary("AccessCache");
initIDs();
}
}
// AccessCache.c
#include "com_study_jnilearn_AccessCache.h"
jmethodID MID_AccessCache_callback;
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs
(JNIEnv *env, jclass cls)
{
printf("initIDs called!!!\n");
MID_AccessCache_callback = (*env)->GetMethodID(env,cls,"callback","()V");
}
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod
(JNIEnv *env, jobject obj)
{
printf("In C Java_com_study_jnilearn_AccessCache_nativeMethod called!!!\n");
(*env)->CallVoidMethod(env, obj, MID_AccessCache_callback);
}
JVM加载AccessCache.class到内存当中之后,会调用该类的静态初始化代码块,即static代码块,先调用System.loadLibrary加载动态库到JVM中,紧接着调用native方法initIDs,会调用用到本地函数Java_com_study_jnilearn_AccessCache_initIDs,在该函数中获取需要缓存的ID,然后存入全局变量当中。下次需要用到这些ID的时候,直接使用全局变量当中的即可,如18行当中调用Java的callback函数。
3. static缓存引用的误区
如果用局部引用+static去缓存引用,局部引用在函数结束后会被VM自动释放掉,这时cls_string成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_com_xxxx_newString这个函数的时候,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的,应该使用全局引用。
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString
(JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len)
{
// ...
jstring jstr = NULL;
static jclass cls_string = NULL;
if (cls_string == NULL) {
jclass local_cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
// 将java.lang.String类的Class引用缓存到全局引用当中
cls_string = (*env)->NewGlobalRef(env, local_cls_string);
// 删除局部引用
(*env)->DeleteLocalRef(env, local_cls_string);
// 再次验证全局引用是否创建成功
if (cls_string == NULL) {
return NULL;
}
}
// ....
return jstr;
}
局部引用表溢出
在Android中局部引用表默认最大容量是512个。这是虚拟机实现的,在程序中应该没办法修改这个数量。所以在一个本地方法中,如果使用了大量的局部引用而没有调用env->DeleteLocalRef及时释放的话,随时都有可能造成程序崩溃的现象。
// 给数组中每个元素赋值
for (i = 0; i < count; ++i) {
memset(buff, 0, sizeof(buff)); // 初始一下缓冲区
sprintf(buff, c_str_sample,i);
jstring newStr = (*env)->NewStringUTF(env, buff);
(*env)->SetObjectArrayElement(env, str_array, i, newStr);
(*env)->DeleteLocalRef(env,newStr); // Warning: 这里如果不手动释放局部引用,很有可能造成局部引用表溢出
}
更多JNI&NDK系列文章,参见:
JNI&NDK开发最佳实践(一):开篇
JNI&NDK开发最佳实践(二):CMake实现调用已有C/C++文件中的本地方法
JNI&NDK开发最佳实践(三):CMake实现调用已有so库中的本地方法
JNI&NDK开发最佳实践(四):JNI数据类型及与Java数据类型的映射关系
JNI&NDK开发最佳实践(五):本地方法的静态注册与动态注册
JNI&NDK开发最佳实践(六):JNI实现本地方法时的数据类型转换
JNI&NDK开发最佳实践(七):JNI之本地方法与java互调
JNI&NDK开发最佳实践(八):JNI局部引用、全局引用和弱全局引用
JNI&NDK开发最佳实践(九):调试篇