32

I have a memory leak that I have isolated to incorrectly disposed direct byte buffers.

ByteBuffer buff = ByteBuffer.allocateDirect(7777777);

The GC collects the objects that harbor these buffers but does not dispose of the buffer itself. If I instantiate enough of the transient objects containing buffers, I get this encouraging message:

java.lang.OutOfMemoryError: Direct buffer memory

I have been searching up this problem and apparently

buff.clear();

and

System.gc();

do not work.

0xCursor
  • 2,242
  • 4
  • 15
  • 33
mglmnc
  • 1,400
  • 2
  • 14
  • 15
  • Are you certain that nothing else is holding a reference to this ByteBuffer? – James Black Dec 06 '09 at 05:01
  • Yes, im pretty sure, I instantiate method level classes that hold the buffers which go out of scope as soon as the method finishes invocation. – mglmnc Dec 06 '09 at 05:33
  • 2
    related: http://stackoverflow.com/questions/1744533/jna-bytebuffer-not-getting-freed-and-causing-c-heap-to-run-out-of-memory/1775542#1775542 – Gregory Pakosz Dec 08 '09 at 21:57

7 Answers7

18

I suspect that somewhere your application has a reference to the ByteBuffer instance(s) and that is preventing it from being garbage collected.

The buffer memory for a direct ByteBuffer is allocated outside of the normal heap (so that the GC doesn't move it!!). However, the ByteBuffer API provides no method for explicitly disposing of / deallocating a buffer. So I assume that the garbage collector will do it ... once it determines that the ByteBuffer object is no longer referenced.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • 1
    If it doesn't, you'll have to use some sort of hack to do it by yourself as I explain here: http://stackoverflow.com/a/26777380/458157 – gouessej Oct 27 '15 at 14:23
17

The DBB will be deallocated once it hits the reference queue, and the finalizer is run. However, as we cannot depend on a finalizer to run, we can use reflection to manually call its "cleaner".

Using reflection:

/**
* DirectByteBuffers are garbage collected by using a phantom reference and a
* reference queue. Every once a while, the JVM checks the reference queue and
* cleans the DirectByteBuffers. However, as this doesn't happen
* immediately after discarding all references to a DirectByteBuffer, it's
* easy to OutOfMemoryError yourself using DirectByteBuffers. This function
* explicitly calls the Cleaner method of a DirectByteBuffer.
* 
* @param toBeDestroyed
*          The DirectByteBuffer that will be "cleaned". Utilizes reflection.
*          
*/
public static void destroyDirectByteBuffer(ByteBuffer toBeDestroyed)
    throws IllegalArgumentException, IllegalAccessException,
    InvocationTargetException, SecurityException, NoSuchMethodException {

  Preconditions.checkArgument(toBeDestroyed.isDirect(),
      "toBeDestroyed isn't direct!");

  Method cleanerMethod = toBeDestroyed.getClass().getMethod("cleaner");
  cleanerMethod.setAccessible(true);
  Object cleaner = cleanerMethod.invoke(toBeDestroyed);
  Method cleanMethod = cleaner.getClass().getMethod("clean");
  cleanMethod.setAccessible(true);
  cleanMethod.invoke(cleaner);

}
Li Pi
  • 486
  • 5
  • 10
  • 2
    Could just cast ByteBuffer to DirectBuffer, then call .cleaner().clean() – ClickerMonkey Jan 16 '13 at 03:25
  • 1
    No you can't. Since DirectByteBuffer is a package level class in java.nio package so will not be visible to your client. This is the correct way to (try) deallocate a direct byte buffer – sutanu dalui May 27 '13 at 08:03
  • 5
    It would be worth documenting that this method could potentially break between JRE versions (although, it would be rather unlikely). Oracle does not guarantee API backward compatibility of internal constructs. – Luke A. Leber Oct 16 '13 at 01:02
  • 2
    Luke is right, your code breaks with Java 1.9 unlike mine: http://stackoverflow.com/a/26777380/458157 – gouessej Oct 27 '15 at 14:24
14

The ByteBuffer documentation says:

A direct byte buffer may be created by invoking the allocateDirect factory method of this class. The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers. The contents of direct buffers may reside outside of the normal garbage-collected heap, and so their impact upon the memory footprint of an application might not be obvious. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations. In general it is best to allocate direct buffers only when they yield a measureable gain in program performance.

In particular, the statement "may reside outside of the normal garbage-collected heap" seems relevant to your example.

Greg Hewgill
  • 951,095
  • 183
  • 1,149
  • 1,285
  • The article of IBM entitled "Thanks for the memory" tells more or less the same thing as I indicated here: http://stackoverflow.com/a/26777380/458157 – gouessej Oct 27 '15 at 14:26
5

The allocated memory is realized through a native libary. This memory will be freed when the ByteBuffer#finalize method is called, iaw when the Buffer is gc'd. Have a look at the allocate() and finalize() implementations of DirectByteBufferImpl.

buff.clear() is not necessary, System.gc() will only help if, like others already mentioned, there's no more reference left to the ByteBuffer object.

Andreas Dolk
  • 113,398
  • 19
  • 180
  • 268
  • 3
    My guess is that System.gc won't help in ANY case. I would expect that the "event" of not having enough buffer memory would trigger a GC in an attempt to free up old buffers. The OOM exception is only thrown when there's not enough buffer memory ... after the GC. – Stephen C Dec 06 '09 at 08:58
  • I'm sorry but showing the source code of GNU Classpath isn't a good idea as each JVM has a different implementation of direct byte buffer and OpenJDK is a lot more used than GNU Classpath. Look at my code and you'll see those differences: http://stackoverflow.com/a/26777380/458157 – gouessej Oct 27 '15 at 14:28
2

Here is a refined implementation that will work for any direct buffer:

public static void destroyBuffer(Buffer buffer) {
    if(buffer.isDirect()) {
        try {
            if(!buffer.getClass().getName().equals("java.nio.DirectByteBuffer")) {
                Field attField = buffer.getClass().getDeclaredField("att");
                attField.setAccessible(true);
                buffer = (Buffer) attField.get(buffer);
            }

            Method cleanerMethod = buffer.getClass().getMethod("cleaner");
            cleanerMethod.setAccessible(true);
            Object cleaner = cleanerMethod.invoke(buffer);
            Method cleanMethod = cleaner.getClass().getMethod("clean");
            cleanMethod.setAccessible(true);
            cleanMethod.invoke(cleaner);
        } catch(Exception e) {
            throw new QuartetRuntimeException("Could not destroy direct buffer " + buffer, e);
        }
    }
}
Antoine CHAMBILLE
  • 1,676
  • 2
  • 13
  • 29
  • 1
    No, it doesn't, it works a bit better as it supports the viewed buffers with Java 1.7 to 1.9 but it breaks with Java <= 1.6 ("att" was called "viewedBuffer" in Java 1.4 to 1.6) and it works only with Oracle Java and OpenJDK unlike my own deallocator helper that supports GNU Classpath, Android Dalvik Virtual Machine, Apache Harmony, Oracle Java and OpenJDK: http://sourceforge.net/p/tuer/code/HEAD/tree/pre_beta/src/main/java/engine/misc/DeallocationHelper.java I don't downvote but you should edit your post. – gouessej Oct 27 '15 at 14:35
1

As long as you are relying on sun (oracle) specific implementation, a better choice than trying to change the visibility of java.nio.DirectByteBuffer is to use the sun.nio.ch.DirectBuffer interface via reflections.

/**
 * Sun specific mechanisms to clean up resources associated with direct byte buffers.
 */
@SuppressWarnings("unchecked")
private static final Class<? extends ByteBuffer> SUN_DIRECT_BUFFER = (Class<? extends ByteBuffer>) lookupClassQuietly("sun.nio.ch.DirectBuffer");

private static final Method SUN_BUFFER_CLEANER;

private static final Method SUN_CLEANER_CLEAN;

static
{
    Method bufferCleaner = null;
    Method cleanerClean = null;
    try
    {
        // operate under the assumption that if the sun direct buffer class exists,
        // all of the sun classes exist
        if (SUN_DIRECT_BUFFER != null)
        {
            bufferCleaner = SUN_DIRECT_BUFFER.getMethod("cleaner", (Class[]) null);
            Class<?> cleanClazz = lookupClassQuietly("sun.misc.Cleaner");
            cleanerClean = cleanClazz.getMethod("clean", (Class[]) null);
        }
    }
    catch (Throwable t)
    {
        t.printStackTrace();
    }
    SUN_BUFFER_CLEANER = bufferCleaner;
    SUN_CLEANER_CLEAN = cleanerClean;
}

public static void releaseDirectByteBuffer(ByteBuffer buffer)
{
    if (SUN_DIRECT_BUFFER != null && SUN_DIRECT_BUFFER.isAssignableFrom(buffer.getClass()))
    {
        try
        {
            Object cleaner = SUN_BUFFER_CLEANER.invoke(buffer, (Object[]) null);
            SUN_CLEANER_CLEAN.invoke(cleaner, (Object[]) null);
        }
        catch (Throwable t)
        {
            logger.trace("Exception occurred attempting to clean up Sun specific DirectByteBuffer.", t);
        }
    }
}
Brett Okken
  • 6,210
  • 1
  • 19
  • 25
0

There are many caveats missing from the existing answers to this question, e.g. requirements when running under JDK 9+ for the module descriptor to contain requires jdk.unsupported, the inability to access MappedByteBuffer.cleaner() without workarounds under JDK 16+ due to the enforcement of strong encapsulation, the requirements if running with a SecurityManager under JDK 7-16, etc. I go over the full details here:

https://stackoverflow.com/a/54046774/3950982

Luke Hutchison
  • 8,186
  • 2
  • 45
  • 40