调试JNI内存泄漏



我编写了一个小型Java JNI程序,它只做从数组arr永久创建元组(arr[i], arr[i+1])的数组。同时,我使用ps aux记录每分钟的RSS内存使用情况,我注意到平均每分钟稳定增加约11KB。JNI代码如下:

/*
* Class:     com_example_Main
* Method:    shift
* Signature: ([Ljava/lang/String;)[Ljava/lang/String;
*/
JNIEXPORT jobjectArray JNICALL Java_com_example_Main_shift(JNIEnv *env, jclass cls, jobjectArray s) {
jsize n = env->GetArrayLength(s);
auto ret = (jobjectArray) env->NewObjectArray(n, env->GetObjectClass(s), env->NewStringUTF(""));
for (auto i = 0; i < n; i++) {
auto js = (jstring) env->GetObjectArrayElement(s, i);
auto next_js = (jstring) env->GetObjectArrayElement(s, (i + 1) % n);
auto tuple = (jobjectArray) env->NewObjectArray(2, env->FindClass("java/lang/String"), env->NewStringUTF(""));
env->SetObjectArrayElement(tuple, 0, js);
env->SetObjectArrayElement(tuple, 1, next_js);
env->SetObjectArrayElement(ret, i, tuple);
}
return ret;
}

据我所见,这里没有需要清理的数据。所以,我真的无法解释为什么记忆会随着时间的推移而改变。

在Java主方法中,我创建了一个带有字符串的长数组,然后在shift之上一遍又一遍地运行:

public class Main {
static {
System.load("/path/to/a.so");
}
private static native String[][] shift(String[] s);
public static void main(String[] args) {
List<String> input = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < 500; j++) {
sb.append(j);
}
input.add(sb.toString());
}
String[] arr = input.toArray(new String[0]);
for (int it = 0; true; it++) {
String[][] next = shift(arr);
arr = Arrays.stream(next)
.map(a -> a[0])
.toArray(String[]::new);
}
}
}

我还编写了该应用程序的另一个版本,其中我用return s替换C++代码,并相应地将响应类型shift更改为String[],并且该应用程序在最初几分钟后内存实际上根本没有增加。这表明内存泄漏隐藏在C++代码的某个地方。

valgrind在这种情况下没有太大帮助,因为它显示了JVM中的数千个错误,但我的代码中没有一个错误。

@Botje正确地指出,从技术上讲,您并没有创建内存泄漏,但在任意长度的循环中调用任何New*都是一个危险信号。首先,GC不能在您之后进行清理,直到您从JNI回调返回JVM。因此,要注意Javaarr的大小,或者在较小的块上拆分为多个JNI调用。其次,JNI对在一个本机调用中可以创建的本地引用数量有限制。简单地说,如果没有配对的DeleteLocalRef或更现代的Push/PopLocalFrame,您可以呼叫New*多少次。默认的预分配是16,限制为65535,这一事实应该会给你一个提示,JNI设计者对这个特性非常敏感。

最新更新