It is actually slightly more complex than that.
The direct-memory is freed when no more direct ByteBuffers refer to it. (Like normal GC behaviour)
The problem however is that the DirectByteBuffer objects are rather small (like a few bytes), so the heap won’t fill much, and the GC won’t get triggered as soon as you might like. Releasing all your DirectByteBuffers that might be refering to massive amounts of memory, will only shrink the heap-usage by a few hundred bytes.
Further, there is this bug (?) in the Sun impl. of sun.misc.Bits, where the code is rougly:
static long total_allocated = 0;
static long max_allocated = 64 * 1024 * 1024;
public static long malloc(int bytes)
{
if(total_allocated + bytes > max_allocated)
System.gc(); // PROBLEM
if(total_allocated + bytes > max_allocated)
throw new OutOfMemoryExzception();
total_allocated += bytes;
return Unsafe.malloc(bytes);
}
public static void free(long base, int bytes)
{
total_allocated -= bytes;
Unsafe.free(base);
}
The problem is that the DirectByteBuffers are tracked by Soft/WeakReferences, and they get enqueued after a gc() so the 2nd gc() will clean them up - and free the memory.
No such luck when you’re only calling System.gc() once (which is btw a hint to the VM…).