On Android, a direct ByteBuffer does not ever seem to release its memory, not even when calling System.gc().
Example: doing
Log.v("?", Long.toString(Debug.getNativeHeapAllocatedSize()));
ByteBuffer buffer = allocateDirect(LARGE_NUMBER);
buffer=null;
System.gc();
Log.v("?", Long.toString(Debug.getNativeHeapAllocatedSize()));
gives two numbers in the log, the second one being at least LARGE_NUMBER larger than the first.
How do I get rid of this leak?
Added:
Following the suggestion by Gregory to handle alloc/free on the C++ side, I then defined
JNIEXPORT jobject JNICALL Java_com_foo_bar_allocNative(JNIEnv* env, jlong size)
{
void* buffer = malloc(size);
jobject directBuffer = env->NewDirectByteBuffer(buffer, size);
jobject globalRef = env->NewGlobalRef(directBuffer);
return globalRef;
}
JNIEXPORT void JNICALL Java_com_foo_bar_freeNative(JNIEnv* env, jobject globalRef)
{
void *buffer = env->GetDirectBufferAddress(globalRef);
free(buffer);
env->DeleteGlobalRef(globalRef);
}
I then get my ByteBuffer on the JAVA side with
ByteBuffer myBuf = allocNative(LARGE_NUMBER);
and free it with
freeNative(myBuf);
Unfortunately, while it does allocate fine, it a) still keeps the memory allocated according to Debug.getNativeHeapAllocatedSize()
and b) leads to an error
W/dalvikvm(26733): JNI: DeleteGlobalRef(0x462b05a0) failed to find entry (valid=1)
I am now thoroughly confused, I thought I at least understood the C++ side of things... Why is free() not returning the memory? And what am I doing wrong with the DeleteGlobalRef()
?
Use the reflection to call java.nio.DirectByteBuffer.free(). I remind you that Android DVM is inspired by Apache Harmony, which supports the method above.
The direct NIO buffers are allocated on the native heap, not on the Java heap managed by the garbage collection. It's up to the developer to release their native memory. It's a bit different with OpenJDK and Oracle Java because they try to call the garbage collector when the creation of a direct NIO buffer fails but there is no guarantee that it helps.
N.B: You'll have to tinker a bit more if you use asFloatBuffer(), asIntBuffer(), ... because only the direct byte buffer can be "freed".
There is no leak.
ByteBuffer.allocateDirect()
allocates memory from the native heap / free store (thinkmalloc()
) which is in turn wrapped in to aByteBuffer
instance.When the
ByteBuffer
instance gets garbage collected, the native memory is reclaimed (otherwise you would leak native memory).You're calling
System.gc()
in hope the native memory is reclaimed immediately. However, callingSystem.gc()
is only a request which explains why your second log statement doesn't tell you memory has been released: it's because it hasn't yet!In your situation, there is apparently enough free memory in the Java heap and the garbage collector decides to do nothing: as a consequence, unreachable
ByteBuffer
instances are not collected yet, their finalizer is not run and native memory is not released.Also, keep in mind this bug in the JVM (not sure how it applies to Dalvik though) where heavy allocation of direct buffers leads to unrecoverable
OutOfMemoryError
.You commented about doing controlling things from JNI. This is actually possible, you could implement the following:
publish a
native ByteBuffer allocateNative(long size)
entry point that:void* buffer = malloc(size)
to allocate native memoryByteBuffer
instance with a call to(*env)->NewDirectByteBuffer(env, buffer, size);
converts theByteBuffer
local reference to a global one with(*env)->NewGlobalRef(env, directBuffer);
publish a
native void disposeNative(ByteBuffer buffer)
entry point that:free()
on the direct buffer address returned by*(env)->GetDirectBufferAddress(env, directBuffer);
deletes the global ref with(*env)->DeleteGlobalRef(env, directBuffer);
Once you call
disposeNative
on the buffer, you're not supposed to use the reference anymore, so it could be very error prone. Reconsider whether you really need such explicit control over the allocation pattern.Forget what I said about global references. Actually global references are a way to store a reference in native code (like in a global variable) so that a further call to JNI methods can use that reference. So you would have for instance:
foo()
which creates a global reference out of a local reference (obtained by creating an object from native side) and stores it in a native global variable (as ajobject
)bar()
which gets thejobject
stored byfoo()
and further processes itbaz()
deletes the global referenceSorry for the confusion.
Not sure if your last comments are old or what Kasper. I did the following...
Then in Java...
and
and everything seems to be working fine for me. Thanks a lot Gregory for this idea. The link to the referenced Bug in the JVM has gone bad.
I was using TurqMage's solution until I tested it on a Android 4.0.3 emulator (Ice Cream Sandwich). For some reason, the call to DeleteGlobalRef fails with a jni warning: JNI WARNING: DeleteGlobalRef on non-global 0x41301ea8 (type=1), followed by a segmentation fault.
I took out the calls to create a NewGlobalRef and DeleteGlobalRef (see below) and it seems to work fine on the Android 4.0.3 emulator.. As it turns out, I'm only using the created byte buffer on the java side, which should hold a java reference to it anyways, so I think the call to NewGlobalRef() was not needed in the first place..