如何在Java中进行直接缓冲区的垃圾回收

32

我有一个内存泄漏问题,它被隔离到了错误地处理直接字节缓冲区。

ByteBuffer buff = ByteBuffer.allocateDirect(7777777);

GC会收集这些缓冲区的对象,但不会处理缓冲区本身。如果我实例化足够多的包含缓冲区的瞬态对象,我会得到这个令人鼓舞的消息:

java.lang.OutOfMemoryError: Direct buffer memory

我一直在搜索这个问题,显然

buff.clear();

System.gc();

都没有用。


你确定没有其他东西在引用这个 ByteBuffer 吗? - James Black
是的,我非常确定,我实例化方法级别的类来持有缓冲区,这些缓冲区在方法完成调用后立即超出作用域。 - mglmnc
2
相关:https://dev59.com/IHI-5IYBdhLWcg3wpqMK#1775542 - Gregory Pakosz
7个回答

18

我怀疑你的应用程序在某个地方引用了ByteBuffer实例,这阻止了它被垃圾回收。

直接ByteBuffer的缓冲区内存是在正常堆之外分配的(以便GC不会移动它!)。然而,ByteBuffer API没有提供显式释放/删除缓冲区的方法。因此,我认为垃圾收集器会在确定ByteBuffer对象不再被引用时进行处理。


1
如果不行的话,你就得自己使用某种方法来实现它,就像我在这里解释的那样:https://dev59.com/LXA75IYBdhLWcg3wDUm2#26777380 - gouessej

17

一旦DBB到达引用队列并运行终结器,它将被解除分配。然而,由于我们不能依赖终结器运行,因此我们可以使用反射手动调用其“清理器”。

使用反射:

/**
* 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);

}

2
可以将ByteBuffer强制转换为DirectBuffer,然后调用.cleaner().clean()。 - ClickerMonkey
1
不可以。由于DirectByteBuffer是java.nio包中的一个包级别类,因此不会对客户端可见。这是(尝试)释放直接字节缓冲区的正确方法。 - sutanu dalui
5
值得记录的是,这种方法在JRE版本之间可能会出现故障(尽管这种情况可能性较小)。 Oracle不保证内部结构的API向后兼容性。 - Luke A. Leber
2
Luke是正确的,你的代码在Java 1.9下会出问题,而我的不会:https://dev59.com/LXA75IYBdhLWcg3wDUm2#26777380 - gouessej

14

ByteBuffer 文档中提到:

可以通过调用这个类的 allocateDirect 工厂方法来创建一个直接字节缓冲区。由此方法返回的缓冲区通常具有比非直接缓冲区更高的分配和释放成本。直接缓冲区的内容可能驻留在正常垃圾回收堆之外,因此它们对于应用程序的内存占用可能不明显。因此,建议仅为长时间存在、受底层系统本地I/O操作影响的大型缓冲区分配直接缓冲区。一般情况下,只有在直接缓冲区能够提供可衡量的程序性能增益时才最好分配直接缓冲区。

特别是,“可能驻留在正常垃圾回收堆之外”这句话似乎与你的例子相关。


IBM的文章《感谢记忆》与我在这里指出的大致相同:https://dev59.com/LXA75IYBdhLWcg3wDUm2#26777380。 - gouessej

5
已分配的内存是通过本机库实现的。当调用ByteBuffer#finalize方法时,即Buffer被垃圾回收时,该内存将被释放。请查看DirectByteBufferImpl的allocate()和finalize()实现。
buff.clear()不是必需的,只有当没有更多的引用指向ByteBuffer对象时,System.gc()才有帮助,正如其他人已经提到的那样。

3
我猜测System.gc在任何情况下都无法有所帮助。我预计,缺少足够的缓冲内存的"事件"将会触发垃圾回收以释放旧的缓冲区。当经过GC后仍然没有足够的缓冲内存时才会抛出OOM异常。 - Stephen C
很抱歉,展示 GNU Classpath 的源代码并不是一个好主意,因为每个 JVM 对于直接字节缓冲区的实现都是不同的,而 OpenJDK 的使用率要比 GNU Classpath 高得多。看看我的代码,你就会发现这些差异:https://dev59.com/LXA75IYBdhLWcg3wDUm2#26777380 - gouessej

2
这里有一个精细的实现,适用于任何直接缓冲区:
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);
        }
    }
}

1
不,它的效果更好,因为它支持Java 1.7到1.9的查看缓冲区,但在Java <= 1.6中会出现问题("att"在Java 1.4到1.6中被称为"viewedBuffer"),而且它只能与Oracle Java和OpenJDK一起使用,而我的释放器助手则支持GNU Classpath、Android Dalvik虚拟机、Apache Harmony、Oracle Java和OpenJDK:http://sourceforge.net/p/tuer/code/HEAD/tree/pre_beta/src/main/java/engine/misc/DeallocationHelper.java 我不会投反对票,但你应该编辑你的帖子。 - gouessej

1
只要您依赖于sun(oracle)特定的实现,比尝试更改java.nio.DirectByteBuffer的可见性更好的选择是通过反射使用sun.nio.ch.DirectBuffer接口。
/**
 * 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);
        }
    }
}

在Java 1.9中不再可能,参见JEP 260。请看上面我的评论... - gouessej

0

现有答案中缺少许多注意事项,例如在JDK 9+下运行时模块描述符必须包含requires jdk.unsupported、在JDK 16+下由于强制执行强封装而无法访问MappedByteBuffer.cleaner(),在JDK 7-16下使用SecurityManager时的要求等。我在这里详细介绍了所有细节:

https://dev59.com/InA85IYBdhLWcg3wCe9Z#54046774


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接