Green 绿色
Green 绿色

Reputation: 2916

Debugging an JNI memory leak

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

Answers (1)

Pavel Zdenek
Pavel Zdenek

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

Related Questions