JNI Reference Leaks: Detection and Fix
The Java Native Interface (JNI) allows Java code to interact with native C/C++ code. Managing memory across this boundary is a common source of critical bugs. The most frequent issue is the JNI reference leak.
When native code receives a reference to a Java object (e.g., passed as an argument or created via NewObject), the Dalvik/ART virtual machine creates a "Local Reference". This prevents the object from being garbage collected while the native method executes.
The Problem
Local references are valid only for the duration of the native method call. If a native method enters a long-running loop or creates a massive number of local references without releasing them, the JNI local reference table can overflow, leading to a direct VM crash (JNI ERROR (app bug): local reference table overflow (max=512)).
// ANTI-PATTERN: Leaking local references in a loop
JNIEXPORT void JNICALL Java_com_example_MyClass_processArray(JNIEnv *env, jobject obj, jobjectArray arr) {
int len = env->GetArrayLength(arr);
for (int i = 0; i < len; i++) {
jobject element = env->GetObjectArrayElement(arr, i); // Creates a local reference
// Do something with element...
// BUG: We forgot to delete the local reference!
// If 'len' is large, the local reference table will overflow.
}
}
The Fix
You must explicitly delete local references when they are no longer needed, especially within loops, using DeleteLocalRef.
// CORRECT PATTERN
JNIEXPORT void JNICALL Java_com_example_MyClass_processArray(JNIEnv *env, jobject obj, jobjectArray arr) {
int len = env->GetArrayLength(arr);
for (int i = 0; i < len; i++) {
jobject element = env->GetObjectArrayElement(arr, i);
// Do something with element...
env->DeleteLocalRef(element); // Explicit cleanup
}
}
If you need a reference to persist across multiple native method calls, you must promote it to a Global Reference (NewGlobalRef) and explicitly delete it later (DeleteGlobalRef).
Critical Section Pitfalls
JNI provides GetPrimitiveArrayCritical and GetStringCritical to access array elements or string characters with potentially lower overhead. These methods might return a direct pointer to the underlying memory, bypassing a copy.
However, entering a critical section often disables Garbage Collection.
The Rule: Code inside a critical section must execute extremely fast and must never make blocking calls (like I/O, waiting on locks, or calling other JNI methods that might trigger GC). Doing so will stall the entire VM.
jint* elements = env->GetIntArrayElements(arr, nullptr);
// Standard access. Safe to block, but might involve a copy.
env->ReleaseIntArrayElements(arr, elements, 0);
void* critical_elements = env->GetPrimitiveArrayCritical(arr, nullptr);
// CRITICAL SECTION START
// Do NOT block here! No sleep(), no complex I/O, no JNI calls!
// Execute fast mathematical operations on critical_elements.
// CRITICAL SECTION END
env->ReleasePrimitiveArrayCritical(arr, critical_elements, 0);
JNI in System Services
Android system services (like ActivityManagerService or WindowManagerService) frequently rely on JNI to interface with lower-level hardware or highly optimized C++ frameworks.
When debugging system services, understanding the JNI boundary is crucial. A crash in libandroid_runtime.so or libbinder.so is often the result of a mismanaged JNI reference or a memory corruption originating in native code invoked by a Java system service.
Use addr2line or ndk-stack to translate native crash addresses in tombstone files (/data/tombstones/) into human-readable source code locations.
Common JNI Bugs in AOSP
- Pending Exceptions: Native code does not automatically halt when a Java exception occurs during a JNI call (e.g.,
CallVoidMethodthrowing an exception). You must check for exceptions usingenv->ExceptionCheck()and handle them, otherwise subsequent JNI calls will crash the VM. - Thread Attachment: A native thread (created via
pthreadsorstd::thread) cannot make JNI calls unless it is explicitly attached to the JVM usingAttachCurrentThread. It must also be detached before it exits usingDetachCurrentThread. - Invalid References: Attempting to use a local reference after the native method has returned, or using an already deleted global reference.