Reputation: 2916
I wrote a small Java JNI program that does nothing but eternally creating an array of tuples (arr[i], arr[i+1])
from an array arr
. Meanwhile, I use ps aux
to record the RSS memory usage every minute and I noticed a steady increase of about 11 KB each minute on average. The JNI code is as follows:
/*
* 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;
}
As far as I can see, there is no data that needed to be cleaned up here. So, I cannot really explain why the memory is changing over time at all.
In the Java main method, I create a long array with strings and then run above shift
over and over:
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);
}
}
}
I also wrote another version of this application where I replace the C++ code by a return s
and change the response type of shift
to String[]
accordingly and that application indeed doesn't increase in memory at all after the first few minutes. That suggests that a memory leak is hidden somewhere in the C++ code.
valgrind
isn't very helpful in this situation because it shows thousands of errors from the JVM, but none from my code.
Upvotes: 0
Views: 1041
Reputation: 7293
@Botje correctly pointed out that you are technically not creating a memory leak, but calling any New*
in a loop of arbitrary length is a red flag anyway. First, GC can't clean up after you until you return from JNI call back to JVM. So beware the size of Java arr
, or split to multiple JNI calls on smaller chunks. Second, JNI has a limit on how many local references you can create in one native call. Simplified, how many times you can call New*
without paired DeleteLocalRef
or more modern Push/PopLocalFrame
. The fact that a default preallocation is 16 and limit 65535 should give you a hint, that JNI designers were quite sensitive about this feature.
Upvotes: 1