【问题标题】:How to garbage collect a direct buffer in Java如何垃圾收集Java中的直接缓冲区
【发布时间】:2010-12-23 16:30:38
【问题描述】:

我有一个内存泄漏,我已将其隔离到错误处置的直接字节缓冲区。

ByteBuffer buff = ByteBuffer.allocateDirect(7777777);

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

java.lang.OutOfMemoryError: 直接缓冲内存

我一直在寻找这个问题,显然

buff.clear();

System.gc();

不工作。

【问题讨论】:

  • 您确定没有其他东西持有对此 ByteBuffer 的引用吗?
  • 是的,我很确定,我实例化了方法级别的类,这些类在方法完成调用后立即保存超出范围的缓冲区。

标签: java memory-leaks buffer bytebuffer


【解决方案1】:

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

直接 ByteBuffer 的缓冲内存分配在普通堆之外(这样 GC 就不会移动它!!)。但是,ByteBuffer API 没有提供显式处理/释放缓冲区的方法。所以我假设垃圾收集器会这样做......一旦它确定不再引用 ByteBuffer 对象。

【讨论】:

【解决方案2】:

ByteBuffer 文档说:

可以通过调用此类的allocateDirect 工厂方法来创建直接字节缓冲区。此方法返回的缓冲区通常比非直接缓冲区具有更高的分配和释放成本。直接缓冲区的内容可能驻留在正常的垃圾收集堆之外,因此它们对应用程序内存占用的影响可能并不明显。因此,建议将直接缓冲区主要分配给受底层系统的本机 I/O 操作影响的大型、长期存在的缓冲区。通常,最好仅在直接缓冲区对程序性能产生可衡量的增益时才分配它们。

特别是,“可能位于正常垃圾收集堆之外”的语句似乎与您的示例相关。

【讨论】:

【解决方案3】:

分配的内存是通过本机库实现的。当调用 ByteBuffer#finalize 方法时,这个内存将被释放,当 Buffer 被 gc'd 时。看看DirectByteBufferImpl 的 allocate() 和 finalize() 实现。

buff.clear() 不是必需的,System.gc() 只有在没有更多对 ByteBuffer 对象的引用时才有帮助。

【讨论】:

  • 我的猜测是 System.gc 在任何情况下都无济于事。我希望没有足够缓冲内存的“事件”会触发 GC 以尝试释放旧缓冲区。只有在 GC 之后没有足够的缓冲内存时才会抛出 OOM 异常。
  • 很抱歉,但显示 GNU Classpath 的源代码并不是一个好主意,因为每个 JVM 都有不同的直接字节缓冲区实现,而且 OpenJDK 比 GNU Classpath 更常用。查看我的代码,您会发现这些差异:stackoverflow.com/a/26777380/458157
【解决方案4】:

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

}

【讨论】:

  • 只能将 ByteBuffer 转换为 DirectBuffer,然后调用 .cleaner().clean()
  • 不,你不能。由于 DirectByteBuffer 是 java.nio 包中的包级类,因此对您的客户端不可见。这是(尝试)释放直接字节缓冲区的正确方法
  • 值得记录的是,此方法可能会在 JRE 版本之间中断(尽管不太可能)。 Oracle 不保证 API 内部结构的向后兼容性。
  • Luke 是对的,您的代码在 Java 1.9 中中断,与我的不同:stackoverflow.com/a/26777380/458157
【解决方案5】:

这是一个适用于任何直接缓冲区的改进实现:

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);
        }
    }
}

【讨论】:

  • 不,它没有,它工作得更好一些,因为它支持 Java 1.7 到 1.9 的查看缓冲区,但它与 Java sourceforge.net/p/tuer/code/HEAD/tree/pre_beta/src/main/java/…我不反对,但你应该编辑你的帖子.
【解决方案6】:

只要您依赖于 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。看看我上面的 cmets...
【解决方案7】:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-08-17
    • 1970-01-01
    • 2010-10-26
    • 1970-01-01
    • 2021-11-28
    • 2010-12-13
    • 2011-02-25
    相关资源
    最近更新 更多