时间过得好快,一转眼写到了第五章,JNI对于许多java程序员来说一般是很陌生的,因为,要想使用JNI必须使用C,然而很多Java程序员都不会C,其实实际上不是这样的。项目中应该是分工协作才是,JNI作为一种粘合剂,将Java代码和C代码粘合在一起。作为一个java程序员,你至少应该会一点点C。
JNI暴露给程序员的并不是真正的引用,而是不透明引用,文档当中称之为opaque references,就是说,我们通过这个 opaque reference 可以间接操作这个引用,但是它并不是引用。我们只能通过JNI函数来访问这个数据结构,这为我们隐藏了依赖于具体虚拟机的实现,我们需要做的只是了解这些接口,并学会如何使用即可。
JNI提供3中类型的不透明引用,global reference , local reference, weak global reference (不做翻译,直接用英文名词), local ref 和global ref有不同的声明周期, local ref 是自动释放的, 而global ref 和weak global ref 直到你手动释放才会被释放。 local ref 和 global ref 持有对象引用都可以保证不被垃圾回收。weak global ref 则允许被垃圾回收。不是所有的引用都能在所有的上下文使用,在native方法返回后,再去访问local ref 是不合法的。
我们用一系列例子来举例一下global ref 和local ref的不同。
local ref: 大多数JNI函数创建的是 local ref, NewObject 创建的就是局部引用, local ref的作用域就是那个native函数的范围,并且就在那次函数调用有效,这种局部变量和C里的局部变量一样。native方法返回这些local ref就会被自动释放。绝对禁止该将local ref保存为static ,并且希望下次方法调用再使用这个local ref。这个类比一下C函数里的static变量,例如,下面的例子是MyNewString的修改版,当然代码是不正确的。
/* This code is illegal */
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclass stringClass = NULL;//我们不应该cache局部引用
jmethodID cid;
jcharArray elemArr;
jstring result;
if (stringClass == NULL) {
stringClass = (*env)->FindClass(env,"java/lang/String");
if (stringClass == NULL) {
return NULL; /* exception thrown */
}
}
/* It is wrong to use the cached stringClass here,because it may be invalid. */
cid = (*env)->GetMethodID(env, stringClass,"<init>", "([C)V");
...
elemArr = (*env)->NewCharArray(env, len);
...
result = (*env)->NewObject(env, stringClass, cid, elemArr);
(*env)->DeleteLocalRef(env, elemArr);
return result;
}
为什么这么做是不对的: 假如C.f() 是一个native方法的调用,这个方法的实现如下:
JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this)
{
char *c_str = ...;
...
return MyNewString(c_str);//native调用native
}
我们重复调用这个方法就可能crash, 因为stringClass引用了一个已经被释放的内存,这个道理就和C的函数里使用static方式保存一个局部变量的地址是一样的,除非你在堆中分配内存。
管理local ref有两种方式,一种是等待函数return后虚拟机自动释放,另一种是调用JNI函数DeleteLocalRef显示释放 。 虚拟机能自动为我们释放内存,我们为什么还需要手动释放?? local ref在生命周期内是占内存的,并且关联的对象不会被垃圾回收,local ref可以在不同的native方法之间传递。局部变量是需要被销毁的,但是local ref关联的对象可以被函数返回,如果不返回,那么这个对象连同local ref 就被销毁了。
即使local ref 可以在native方法之间传递,但是,这个传递也不是没有限制,唯一的限制就是不能跨线程。线程是local ref最后的作用域。
global ref:
使用全局引用,来修改我们的例子:这样的代码,多次调用不会有问题
/* This code is OK */
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jclass stringClass = NULL;
...
if (stringClass == NULL) {
jclass localRefCls = (*env)->FindClass(env, "java/lang/String");//这是局部引用 local ref
if (localRefCls == NULL) {
return NULL; /* exception thrown */
}
/* Create a global reference */
stringClass = (*env)->NewGlobalRef(env, localRefCls);//这是全局引用 global ref
/* The local reference is no longer useful */
(*env)->DeleteLocalRef(env, localRefCls);//释放局部引用
/* Is the global reference created successfully? */
if (stringClass == NULL) {
return NULL; /* out of memory exception thrown */
}
}
...
}
weak global ref:
是在Java 2 SDK release 1.2. 中引入的,创建NewGlobalWeakRef 释放DeleteGlobalWeakRef , global ref 和weak global ref 都可以跨线程。 和global ref不同的是,weak global ref 不保证关联的对象不被垃圾回收。上个例子,我们可以将 stringClass 用作weak global ref,因为,java.lang.String 是一个系统类,永远不会被垃圾回收。所以也就无所谓使用global ref 或者weak global ref了。
什么时候weak global ref 有用呢? 看下面例子:mypkg.MyCls.f( )
JNIEXPORT void JNICALL
Java_mypkg_MyCls_f(JNIEnv *env, jobject self)
{
static jclass myCls2 = NULL;
if (myCls2 == NULL) {
jclass myCls2Local = (*env)->FindClass(env, "mypkg/MyCls2");
if (myCls2Local == NULL) {
return; /* can’t find class */
}
myCls2 = NewWeakGlobalRef(env, myCls2Local);
if (myCls2 == NULL) {
return; /* out of memory */
}
}
... /* use myCls2 */
}
如果我们不保证weak global ref关联的对象不被垃圾回收,这个例子中,即使mypkg.MyCls2 的引用被声明成了weak global ref 它仍然可以被虚拟机卸载,这个系统类不同。
后面再讲述,怎么判断这个weak global ref 关联的对象是否还活着。
引用的比较,比较的是指向对象是否是同一个对象:
对于global ref 和local ref weak global ref: (*env)->IsSameObject(env, obj1, obj2) 返回JNI_TRUE JNI_FALSE
判断引用是否为空(*env)->IsSameObject(env, obj, NULL) 或者 obj == NULL, 但是对于weak global ref 情况稍有不同,假如(*env)->IsSameObject(env, wobj, NULL) , wobj是weak global ref 那么如果wobj被回收了,就会返回JNI_TRUE,返回JNI_FALSE 说明对象还活着。
释放local ref:
我们一般不需要手动释放local ref ,虚拟机会为我们自动管理,但是手动管理可以更有效的利用内存。如下情形,主动释放,会更有效的利用内存,而不会内存耗尽:
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
(*env)->DeleteLocalRef(env, jstr);
}
加入你的native方法不返回,而是进入一个死循环,那么主动释放就会更有效利用内存。 总之文档上举例都是native函数很久才返回的例子,我们应当保持良好的习惯,不使用的尽量手动释放,除非你确定这个native方法很快会返回。
Java 2 SDK Release 1.2以后引入了一些新的api来管理local ref : EnsureLocalCapacity NewLocalRef, PushLocalFrame, PopLocalFrame.
JNI规范规定:虚拟机自动保证每个native方法能够创建至少16个local ref ,经验告诉我们,对于一些交互不是很复杂的native调用,这么大的空间足够用了,如果不够用,你可以调用EnsureLocalCapacity来保证空间够用,我们可以使用如下方式来测试内存是否够用:
/* The number of local references to be created is equal to the length of the array. */
if ((*env)->EnsureLocalCapacity(env, len)) < 0) {
... /* out of memory */
}
for (i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
/* DeleteLocalRef is no longer necessary */
}
跟之前的版本比,这个版本可能会消耗更多的内存,即使我们DeleteLocalRef。
我们还可以使用Push/PopLocalFrame创建嵌套作用域的local ref ,例如,我们可以重写上面的例子:
#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) {
... /* out of memory */
}
jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* process jstr */
(*env)->PopLocalFrame(env, NULL);
}
PushLocalFrame创建一个新的作用域,在这个作用域你可以使用最多N_REFS个local ref,在PopLocalFrame调用后,这个作用域的local ref会被释放。使用Push/PopLocalFrame的好处是,你可以批量管理。NewLocalRef函数在我们写utils函数的时候比较有用,后面详细介绍。
对于局部引用 local ref 保持良好的习惯,不适用就释放就可以了,文档就是这个意思。如果你的习惯良好,在使用Push/PopLocalFrame的时候就会降低因内存不够退出的可能性。
释放global ref:调用DeleteGlobalRef DeleteWeakGlobalRef。对于weak global ref需要说明:不使用尽量手动释放,因为,如果你不调用delete手动释放,虽然垃圾回收会把weak global ref指向的对象回收掉,但是,引用本身占的内存并不会被释放。
引用管理的原则:目标是避免不必要的内存使用,和不必要的内存不释放。这是核心。
有两种类型的native代码,一类是直接实现native方法的代码,还有一类是工具代码utils,对于前者,只需要注意一下方法是否返回,多长时间返回,有没有循环,循环多长时间,创建多少local ref,避免不必要的local ref创建。native方法返回前,还有16个local ref没释放是可以接受的。native方法不要创建过多global ref 或global weak ref , 因为这些东西在方法返回前不会被释放,对于global weak ref 至少是释放不干净的。实现utils方法的时候,你必须保证每一个local ref都被手动释放,千万不要漏掉,因为utils是经常被调用的,重复调用的,并且在哪调用的我们是不知道的。当调用utils方法,如果方法返回原生类型数据,我们必须保证,这个调用没有产生额外的local, global, or weak global references , 当方法返回引用类型数据,除了返回的引用类型数据,任何其他的引用local, global, or weak global references 都应该被释放。
这些utils方法可以利用 global or weak global references缓存一些数据,因为只有第一次调用会创建这些对象,因此不会造成内存泄漏。如果utils返回引用,确保这个方法只返回一种类型的引用,不要这种情况下调用返回一个local ref ,那种情况下又返回一个global ref,这样创建者就能够维护好返回的引用。例如GetInfoString是一个utils方法,且被多次调用:
while (JNI_TRUE) {
jstring infoString = GetInfoString(info);
... /* process infoString */
/* we need to call DeleteLocalRef, DeleteGlobalRef, or DeleteWeakGlobalRef depending on the type of
reference returned by GetInfoString. */
}
Java 2 SDK release 1.2后,NewLocalRef能够保证返回的总是local ref,下面的例子对MyNewString函数做了一点修改,一个经常使用的字符串"CommonString"被缓存成globa ref
jstring MyNewString(JNIEnv *env, jchar *chars, jint len)
{
static jstring result;
/* wstrncmp compares two Unicode strings */
if (wstrncmp("CommonString", chars, len) == 0) {
/* refers to the global ref caching "CommonString" */
static jstring cachedString = NULL;
if (cachedString == NULL) {
/* create cachedString for the first time */
jstring cachedStringLocal = ... ;
/* cache the result in a global reference */
cachedString = (*env)->NewGlobalRef(env, cachedStringLocal);//缓存动作
}
return (*env)->NewLocalRef(env, cachedString);//返回缓存的字符串,注意作为local ref返回
}
... /* create the string as a local reference and store in result as a local reference */
return result;
}
正常的话,会返回一个local ref,作为调用者,得到的永远是local ref, 这个例子也告诉我们,我们可以创建一个global ref的local ref
Push/PopLocalFrame管理local ref实在是太方便了 , native方法开始执行的时候调用PushLocalFrame ,方法返回前调用PopLocalFrame,就能保证之间使用的local ref都能被释放,方便吧!这个方法太好了,文档强烈推荐我们使用。
看一个代码片段来理解一下:
jobject f(JNIEnv *env, ...)
{
jobject result;
if ((*env)->PushLocalFrame(env, 10) < 0) {
/* frame not pushed, no PopLocalFrame needed */
return NULL;
}
...
result = ...;
if (...) {
/* remember to pop local frame before return */
result = (*env)->PopLocalFrame(env, result);
return result;
}
...
result = (*env)->PopLocalFrame(env, result);
/* normal return */
return result;
}
看到了吧,在每个return之前都有PushLocalFrame, 也就是说,PushLocalFrame PopLocalFrame是成对调用的。你调用了PushLocalFrame就必须调用PopLocalFrame,否则发生什么谁也无法保证,也许虚拟机会挂掉。
上面的例子同时也表明了为什么有时候需要传PopLocalFrame的第二个参数,result局部引用最开始是在PushLocalFrame创建的帧中创建。PopLocalFrame在最顶部的帧返回之前将第二个参数 result (存在前一个帧当中)转成一个local ref 。 可以这么理解:push1 push2 pop2 pop1, 一个local reference要想在两个frame中都用,那么在PopLocalFrame的第二个参数传入那个需要继续使用的local ref。也就是说除了第二个参数,其余的local ref 全部释放,这样我们就可以return result了。