2.5.3 全局引用、弱全局引用和局部引用
Java对象的生命周期由虚拟机管理,虚拟机内部维护一个对象的引用计数,如果一个对象的引用计数为0,这个对象将被垃圾回收器回收并释放内存。这里就有一个问题,如果Java对象中使用了Native方法,那会对对象的生命周期产生什么影响呢?
回答这个问题前,先看Log系统的例子。代码如下:
//static jobject clazz_ref1=NULL;方法1加入的code,见下文对方法1的解释
static jboolean android_util_Log_isLoggable(JNIEnv*env, jobject clazz,
jstring tag, jint level)
{
……
//clazz_ref1=clazz;方法1加入的code
//static jobject clazz_ref2=NULL;方法2加入的code
//clazz_ref2=clazz;方法2加入的code
if((strlen(chars)+sizeof(LOG_NAMESPACE))>PROPERTY_KEY_MAX){
……//异常处理代码
}else{
result=isLoggable(chars, level);
}
……
}
这部分代码中,并没有操作传进来的jobject对象,在这里对其进行修改,加入自己的代码保存传进来的jobject对象。要达到保存jobject对象的目的,C/C++程序员有两种方法:
方法1 在方法外加入全局变量,并在方法内赋值。
方法2 在方法内加入静态变量,并赋值。
这两种方法能达到我们的目的吗?
很不幸,答案是不能,而且后果很严重。
因为这样做,虚拟机无法跟踪该对象的引用计数,相当于没有增加引用计数。如果jobject已经被虚拟机回收,clazz_ref1和clazz_ref2将引用一个野指针,C/C++程序员应该知道野指针的问题有多严重。
那既然传统的方法无法保存对象,我们又该怎么做呢?
既然赋值操作无法通知虚拟机增加对象的引用计数,那是不是应该想到JNIEnv能替我们做些什么?因为到目前为止,我们能操作的只有这个接口。
幸运的是,JNIEnv已经为我们提供了解决方案:局部引用、全局引用和弱全局引用。
先来看JNI规范中是怎么定义这三种引用的。
1.局部引用
可以增加引用计数,作用范围为本线程,生命周期为一次Native调用。局部引用包括多数JNI函数创建的引用,Native方法返回值和参数。局部引用只在创建它的Native方法的线程中有效,并且只在Native方法的一次调用中有效,在该方法返回后,被虚拟机回收(不同于C中的局部变量,返回后会立即回收)。
2.全局引用
可以增加引用计数。作用范围为多线程,多个Native方法,生命周期到显式释放。全局引用通过JNI函数NewGlobalRef创建,并通过DeleteGlobalRef释放。如果程序员不显式释放,将永远不会被垃圾回收。
3.弱全局引用
不能增加引用计数。作用范围为多线程,多个Native方法,生命周期到显式释放。不过其对应的Java对象生命周期依然取决于虚拟机,意思是即便弱全局引用没有被释放,其引用的Java对象可能已经被释放。弱全局引用通过JNI函数NewWeakGlobalRef创建,并通过DeleteWeakGlobalRef释放。弱全局引用的优点是:既可以保存对象,又不会阻止该对象被回收。
注意 使用弱全局引用的时候,一定要注意:它所指向的对象可能已经被回收了。JNI提供了IsSameObject函数用来判断弱引用对应的对象是否已经被回收,方法是用弱全局引用和NULL进行比较,如果返回JNI_TRUE,则说明弱全局引用指向的对象已经被回收。
IsSameObject的方法声明如下。
在C中:
jboolean(IsSameObject)(JNIEnv,jobject, jobject);
在C++中:
jboolean IsSameObject(jobject ref1,jobject ref2);
假设有一个弱引用weak_gref,可以按照如下方法使用:
if(env->IsSameObject(weak_gref, NULL)==JNI_TRUE)
{
//do something with weak_gref
}
既然已经知道了JNI中如何保存对象,我们继续修改代码,引入全局引用达到保存对象的目的。修改如下:
static jobject g_clazz_ref=NULL;
static jboolean android_util_Log_isLoggable(JNIEnv*env,
jobject clazz, jstring tag, jint level)
{
……
g_clazz_ref=env->NewGlobalRef(clazz);
if((strlen(chars)+sizeof(LOG_NAMESPACE))>PROPERTY_KEY_MAX){
……
}else{
result=isLoggable(chars, level);
}
……
}
//一定要记住,在不使用该类的时候显式删除
env->DeleteGlobalRef(g_clazz_ref);
Android中对局部引用和全局引用的使用都有一定限制。如果引用超过一定数量,或者使用不当,非常容易引起内存不足和内存泄露问题。
对于全局引用,默认不能超过2000个,否则会出现内存不足的警告。如果在Dalvik的启动参数dalvik.vm.checkjni中设置打开checkjni的选项,Dalvik将监控全局引用的数量,如果超过2000,在logcat中会看到"GREF overflow",提示内存不足。GREF便是全局引用的缩写。