用 jProfiler 分析 Java swing 程序中的内存泄漏问题时, 我发现内存中 JFrame 实例的数量一直在增加。
各个 frame 被打开(opened),然后被关闭(closed)。
通过 jProfiler, 并查看GC Root时, 只找到一项: ‘JNI Global reference’。
这是什么意思? 为什么他 hang 住了所有的 frame 实例?
请查看《维基百科》中关于 Java本地接口 的介绍, 本质上它允许 Java程序 和系统库之间进行通信。
JNI全局引用很容易造成内存泄漏, 因为它们不能被自动垃圾收集所清理, 程序员必须显式地释放它们. 如果你没有编写任何JNI代码, 那么狠可能是使用的库中存在内存泄漏。
修正: 请参考关于 local vs. global references 的更多信息. 其中介绍了为什么要使用全局引用(以及如何进行释放)。
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 */
}
stackoverflow: What is ‘JNI Global reference’
Local and Global References
翻译人员: 铁锚 http://blog.csdn.net/renfufei
翻译时间: 2017年01月28日