问题描述

用 jProfiler 分析 Java swing 程序中的内存泄漏问题时, 我发现内存中 JFrame 实例的数量一直在增加。

各个 frame 被打开(opened),然后被关闭(closed)。

通过 jProfiler, 并查看GC Root时, 只找到一项: ‘JNI Global reference’。

这是什么意思? 为什么他 hang 住了所有的 frame 实例?

回答1

请查看《维基百科》中关于 ​​Java本地接口​​ 的介绍, 本质上它允许 Java程序 和系统库之间进行通信。

JNI全局引用很容易造成内存泄漏, 因为它们不能被自动垃圾收集所清理, 程序员必须显式地释放它们. 如果你没有编写任何JNI代码, 那么狠可能是使用的库中存在内存泄漏。

修正:​ 请参考关于 ​​ local vs. global references​​ 的更多信息. 其中介绍了为什么要使用全局引用(以及如何进行释放)。

回答2

JNI全局引用(JNI global reference), 是从 “native” 代码指向堆内存中Java对象的引用. 其存在的目的是阻止垃圾收集器, 不要误将 native 代码中仍在使用的对象给回收了, 假如这些Java对象没有Java代码引用到他们的话。

一个 JFrame 实例就是一个窗口(​​java.awt.Window​​​), 并关联到一个本地的 ​​Window​​​ 对象。如果某个特定 JFrame 实例的任务已经结束,那么就应该调用 ​​dispose()​​ 方法来执行清理。

我不确定本地代码是否创建了全局引用来指向 JFrame, 但应该是这样没错。如果确实创建了全局引用, 那就会阻止 JFrame 对象被GC回收. 如果程序中创建了很多 Window 对象(或其子类对象), 而又没有调用 dispose(), 则他们永远都不会被GC回收掉, 这就造成了隐形的内存泄露。

局部引用和全局引用简介

JNI为所有传递给 native 方法的对象型参数创建了引用, 同时也对所有从JNI函数返回的对象创建引用。

这些引用会阻止Java对象被GC清理。为了确保Java对象最终被释放, JNI默认创建的是局部引用(local references). 当本机方法返回时, 其创建的局部引用就会失效。当然, native 方法不应该将局部引用存到其他地方, 妄图在后续调用中进行重用。

例如,以下程序(​​FieldAccess.c​​ 中的一种变体native方法) 错误地将Java类的 ID field 缓存起来, 期待不必每次都通过字段名称和签名去搜索 ID field ,:

/* !!! 这是一段问题代码 */
static jclass cls = 0;
static jfieldID fid;

JNIEXPORT void JNICALL
Java_FieldAccess_accessFields(JNIEnv *env, jobject obj)
{
...
if (cls == 0) {
cls = (*env)->GetObjectClass(env, obj);
if (cls == 0)
... /* error */
fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
}
... /* access the field using cls and fid */
}

这个程序是错误的,因为从 ​​GetObjectClass​​​ 返回的局部引用只在 native 方法返回前才有效。第二次进入 ​​Java_FieldAccess_accessField​​ 方法时, 将会引用到一个无效的 local reference。 这会引起错误的结果甚至导致 JVM 崩溃。

要解决这种问题, 可以创建全局引用(global reference)。全局引用将一直有效,直到显式释放:

/* 本段代码是OK的 */
static jclass cls = 0;
static jfieldID fid;

JNIEXPORT void JNICALL
Java_FieldAccess_accessFields(JNIEnv *env, jobject obj)
{
...
if (cls == 0) {
jclass cls1 = (*env)->GetObjectClass(env, obj);
if (cls1 == 0)
... /* error */
cls = (*env)->NewGlobalRef(env, cls1);
if (cls == 0)
... /* error */
fid = (*env)->GetStaticFieldID(env, cls, "si", "I");
}
... /* access the field using cls and fid */
}

全局引用一直存在,直到Java类被卸载之后。 因此保证了在下次用到Java类的ID字段时其一直有效。 native 代码不再使用全局引用时必须调用 ​​DeleteGlobalRefs​​ 函数; 否则,对应的Java对象(如 cls引用的Java类)永远都不会被卸载。

在大多数情况下, native 程序员应该依靠VM来释放所有的局部引用. 在某些特殊情况下,native 代码也可以调用 ​​DeleteLocalRef​​ 函数来显式地删除局部引用。这些情况包括:

引用了某个庞大的Java对象, 不想等当前 native 方法返回时才让Java对象被GC回收。例如,在下面的程序中, GC 可以释放​​lref​​所引用的Java对象, 而此时 lengthyComputation 方法还在执行中:

lref = ...            /* a large Java object */
... /* last use of lref */
(*env)->DeleteLocalRef(env, lref);

lengthyComputation(); /* may take some time */

return; /* all local refs will now be freed */
}

也可能会在 native 方法中创建大量的局部引用。这很可能会导致 JNI local reference table 溢出. 这时候删除那些不用的局部引用是挺有必要的。 例如下面的程序中, native 代码遍历一个由 java字符串组成的大数组. 每次迭代之后,都可以释放字符串元素的局部引用:

for(i = 0; i < len; i++) {
jstring jstr = (*env)->GetObjectArrayElement(env, arr, i);
... /* processes jstr */
(*env)->DeleteLocalRef(env, jstr); /* no longer needs jstr */
}

参考:

翻译时间: 2017年01月28日