Managing JNI References
Proper memory management in JNI is critical. Because JNI spans the managed Java heap (garbage collected) and the unmanaged native heap (manual memory management), references bridge the gap. Failing to manage these references correctly leads to memory leaks or catastrophic crashes.
Local vs. Global vs. Weak Global References
JNI provides three types of references to prevent the Dalvik/ART garbage collector from prematurely reclaiming objects used by native code.
- Local References: Created automatically by most JNI functions (e.g.,
NewStringUTF,FindClass). They are valid only for the duration of the current native method call and only in the thread that created them. Once the native method returns to Java, all local references are automatically deleted. - Global References: Created explicitly. They remain valid across multiple native method calls and across different threads. They prevent the referenced object from being garbage collected until explicitly deleted.
- Weak Global References: Similar to global references, but they do not prevent the object from being garbage collected. Before using a weak global reference, you must check if the object has been reclaimed.
Reference Table Overflow: A Common Pitfall
The JVM maintains a table of local references for each thread. By default, Android limits this table (typically to 512 entries). If your native code loops and creates many local references without releasing them, the table will overflow, causing the app to crash with a JNI ERROR: local reference table overflow log.
// BAD CODE: Causes local reference table overflow
for (int i = 0; i < 10000; i++) {
// NewStringUTF creates a new local reference on every iteration!
jstring str = env->NewStringUTF("Example");
// Do something with str
}
Using DeleteLocalRef and NewGlobalRef
To prevent overflows, you must manually delete local references inside large loops or when they are no longer needed. To persist an object across JNI calls, promote it to a global reference.
// GOOD CODE: Managing local references
for (int i = 0; i < 10000; i++) {
jstring str = env->NewStringUTF("Example");
// Do something with str
env->DeleteLocalRef(str); // Release the local reference
}
// Creating a Global Reference
jobject localObj = env->GetObjectArrayElement(array, 0);
jobject globalObj = env->NewGlobalRef(localObj);
env->DeleteLocalRef(localObj); // Clean up the local one
// ... later, when globalObj is no longer needed ...
env->DeleteGlobalRef(globalObj);
JNI Critical Sections: GetPrimitiveArrayCritical
When passing large arrays of primitive types between Java and native code, JNI might copy the data. To avoid copying and directly access the memory in the Java heap, JNI offers "critical" access functions.
GetPrimitiveArrayCritical attempts to provide a direct pointer to the array elements. However, while holding this pointer, you are in a "critical section." You must not make any other JNI calls, and you must release the pointer as quickly as possible using ReleasePrimitiveArrayCritical. The Garbage Collector might be paused during this time.
jintArray javaArray = ...;
jint* nativeArray = (jint*) env->GetPrimitiveArrayCritical(javaArray, nullptr);
if (nativeArray != nullptr) {
// CRITICAL SECTION: Fast, direct memory access.
// DO NOT make other JNI calls here! DO NOT block!
for (int i = 0; i < length; i++) {
nativeArray[i] *= 2;
}
env->ReleasePrimitiveArrayCritical(javaArray, nativeArray, 0);
}