ByteBuffer not releasing memory
Solution 1
There is no leak.
ByteBuffer.allocateDirect()
allocates memory from the native heap / free store (think malloc()
) which is in turn wrapped in to a ByteBuffer
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, calling System.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:- calls
void* buffer = malloc(size)
to allocate native memory - wraps the newly allocated array into a
ByteBuffer
instance with a call to(*env)->NewDirectByteBuffer(env, buffer, size);
converts theByteBuffer
local reference to a global one with(*env)->NewGlobalRef(env, directBuffer);
- calls
-
publish a
native void disposeNative(ByteBuffer buffer)
entry point that:- calls
free()
on the direct buffer address returned by*(env)->GetDirectBufferAddress(env, directBuffer);
deletes the global ref with(*env)->DeleteGlobalRef(env, directBuffer);
- calls
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:
- from Java, call native method
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
) - once back, from Java again, call native method
bar()
which gets thejobject
stored byfoo()
and further processes it - finally, still from Java, a last call to native
baz()
deletes the global reference
Sorry for the confusion.
Solution 2
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..
JNIEXPORT jobject JNICALL Java_com_foo_allocNativeBuffer(JNIEnv* env, jobject thiz, jlong size)
{
void* buffer = malloc(size);
jobject directBuffer = env->NewDirectByteBuffer(buffer, size);
return directBuffer;
}
JNIEXPORT void JNICALL Java_comfoo_freeNativeBuffer(JNIEnv* env, jobject thiz, jobject bufferRef)
{
void *buffer = env->GetDirectBufferAddress(bufferRef);
free(buffer);
}
Related videos on Youtube
Kasper Peeters
I am a physics/maths lecturer at Durham University in the UK. Interested in scientific software development in general. Author of tree.hh (a tree data structure library for C++) and cadabra, a symbolic computer algebra system.
Updated on July 09, 2022Comments
-
Kasper Peeters almost 2 years
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 errorW/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()
?-
Jon Willis about 13 yearsallocateDirect allocates memory that is guaranteed not to be garbage collected.
-
Gregory Pakosz about 13 yearsJon >
allocateDirect
allocates memory that is guaranteed not to be relocated by the garbage collector. But as soon as theByteBuffer
gets collected, the native memory is reclaimed.
-
-
Kasper Peeters about 13 yearsThat is extremely helpful, thanks! Yes, I am having the problem related to the VM bug. I prefer to have explicit control, as the alternative is to allocate a buffer with an estimated size, and that estimate is almost always far larger than what I actually need.
-
Kasper Peeters about 13 yearsI must be missing something about the form of disposeNative you suggested. Would you mind having a look at the added comment? Thanks a lot.
-
Gregory Pakosz about 13 yearshmm, i didn't try it myself indeed. remove calls to NewGlobalRef and DeleteGlobalRef
-
Kasper Peeters about 13 yearsI never managed to get Debug.getNativeHeapAllocatedSize() to report more free memory after the free(). Does it do that for you? I did exactly the same thing as you wrote. In the end I went for a solution which is less memory hungry and forgot about this whole 'alloc/dealloc on JNI side' thing (the whole story has thoroughly convinced me that garbage collection is a bad thing, but that's a different story...)
-
TurqMage about 13 yearsYep I was having the same problem where getNativeHeapAllocatedSize() would never go down. When I put that code in the Alloc'd size dropped as one would expect. I am compiling to SDK 2.1 with NDK R5 and running on an HTC Incredible that is running Android 2.1.
-
TurqMage about 13 yearsWere you changing the ByteBuffer to another buffer? I just ran into this problem. I was returning ByteBuffers, using .asIntBuffer and storing them only as IntBuffers. When it came time to free the buffers the GlobalRefs wouldn't free correctly. Keeping the reference to the original ByteBuffer and freeing that fixed the problem.
-
Alex Cohn over 11 years@Gregory Pakosz: This bug somehow went viral. People keep asking the same questions, having copied these lines without thinking. See e.g. stackoverflow.com/questions/12803493/…
-
gouessej over 8 years@TurqMage Only the real direct byte buffer can be freed, look at the source code of OpenJDK and Apache Harmony to understand why. When you create a view on a direct byte buffer, only the viewed buffer really allocates some memory.