16

我正在使用直接缓冲区 (java.nio) 来存储 JOGL 的顶点信息。这些缓冲区很大,并且在应用程序生命周期中会多次更换。内存没有及时释放,经过几次更换后内存不足。

似乎没有使用 java.nio 的缓冲区类解除分配的好方法。我的问题是这样的:

JOGL 中是否有一些方法可以删除直接缓冲区?我正在研究 glDeleteBuffer(),但似乎这只会从视频卡内存中删除缓冲区。

谢谢

4

6 回答 6

21

直接 NIO 缓冲区使用非托管内存。这意味着它们是在本机堆上分配的,而不是在 Java 堆上。因此,只有当 JVM 在 Java 堆上用完内存时,它们才会被释放,而不是在本机堆上。换句话说,它是不受管理的=由您来管理它们。不鼓励强制垃圾收集,并且大多数情况下不会解决这个问题。

当你知道一个直接的 NIO 缓冲区已经对你无用时,你必须使用它的 sun.misc.Cleaner 释放它的本机内存(StaxMan 是对的)并调用 clean()(Apache Harmony 除外),调用 free() (使用 Apache Harmony)或使用更好的公共 API 来做到这一点(可能在 Java > 12 中,扩展 AutoCloseable 的 AutoCleaning?)。

这不是 JOGL 的工作,您可以使用纯 Java 代码自己完成。我的示例在 GPL v2 下,这个示例在更宽松的许可证下。

编辑:我的最新示例甚至适用于 Java 1.9,并支持 OpenJDK、Oracle Java、Sun Java、Apache Harmony、GNU Classpath 和 Android。您可能必须删除一些语法糖才能使其与 Java < 1.7 一起使用(多捕获、菱形和泛型)。

参考:http ://www.ibm.com/developerworks/library/j-nativememory-linux/

Direct ByteBuffer 对象会自动清理它们的本地缓冲区,但只能作为 Java 堆 GC 的一部分这样做——因此它们不会自动响应本地堆上的压力。GC 仅在 Java 堆变得如此满以至于无法为堆分配请求提供服务或 Java 应用程序显式请求它时才会发生(不推荐,因为它会导致性能问题)。

参考:http ://docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html#direct

直接缓冲区的内容可能驻留在正常的垃圾收集堆之外

该解决方案集成在 Java 14 中:

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   ...
}

您可以通过调用MemorySegment.ofByteBuffer(ByteBuffer)将字节缓冲区包装到内存段中,获取其内存地址并在 Java 16 中释放它(这是一种受限方法):

CLinker.getInstance().freeMemoryRestricted(MemorySegment.ofByteBuffer(myByteBuffer).address());

请注意,在许多重要的情况下,您仍然需要使用反射来找到可以释放的缓冲区,通常是当您的直接 NIO 缓冲区不是 ByteBuffer 时。

注意:sun.misc.Cleaner 已在 Java 1.9 中的模块“java.base”中移至jdk.internal.ref.Cleaner,后者实现了 java.lang.Runnable(感谢 Alan Bateman 提醒我不同之处)很短的时间,但它不再是这种情况。你必须调用 sun.misc.Unsafe.invokeCleaner(),它是在 JogAmp 的 Gluegen 中完成的。我更喜欢将 Cleaner 用作 Runnable,因为它避免了依赖 sun.misc.Unsafe 但它现在不起作用。

我的最后一个建议适用于 Java 9、10、11 和12

我最新的示例需要使用孵化功能(需要 Java >= 14),但非常简单。

在更宽松的许可下,Lucene 中有一个很好的例子。

于 2014-11-06T10:41:11.640 回答
3

直接缓冲区很棘手,并且没有通常的垃圾收集保证 - 请参阅更多详细信息: http: //docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html#direct

如果您遇到问题,我建议分配一次并重新使用缓冲区,而不是重复分配和解除分配。

于 2010-08-16T21:29:23.567 回答
2

完成释放的方式很糟糕——一个软引用基本上被插入到一个 Cleaner 对象中,然后当拥有 ByteBuffer 被垃圾回收时它会进行释放。但这并不能真正保证及时调用。

于 2011-12-11T05:43:10.697 回答
0

释放直接缓冲区是垃圾收集器在标记 ByteBuffer 对象一段时间后完成的工作。

您可以在删除对缓冲区的最后一个引用后立即尝试调用 gc。至少有可能内存会更快地释放。

于 2010-08-16T21:19:28.993 回答
0

使用gouessej 的回答中的信息,我能够将这个实用程序类放在一起以释放给定 ByteBuffer 的直接内存分配。当然,这应该只作为最后的手段使用,而不应该真正用于生产代码。

在 Java SE 版本 10.0.2 中测试和工作。

public final class BufferUtil {

    //various buffer creation utility functions etc. etc.

    protected static final sun.misc.Unsafe unsafe = AccessController.doPrivileged(new PrivilegedAction<sun.misc.Unsafe>() {
        @Override
        public Unsafe run() {
            try {
                Field f = Unsafe.class.getDeclaredField("theUnsafe");
                f.setAccessible(true);
                return (Unsafe) f.get(null);
            } catch(NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException ex) {
                throw new RuntimeException(ex);
            }
        }
    });

    /** Frees the specified buffer's direct memory allocation.<br>
     * The buffer should not be used after calling this method; you should
     * instead allow it to be garbage-collected by removing all references of it
     * from your program.
     * 
     * @param directBuffer The direct buffer whose memory allocation will be
     *            freed
     * @return Whether or not the memory allocation was freed */
    public static final boolean freeDirectBufferMemory(ByteBuffer directBuffer) {
        if(!directBuffer.isDirect()) {
            return false;
        }
        try {
            unsafe.invokeCleaner(directBuffer);
            return true;
        } catch(IllegalArgumentException ex) {
            ex.printStackTrace();
            return false;
        }
    }

}
于 2020-06-13T03:35:04.750 回答
-1

与其滥用非公共 API 的反射,您可以完全在受支持的公共 API 内轻松完成此操作。

编写一些用 NewDirectByteBuffer 包装 malloc 的 JNI(记得设置顺序),以及一个类似的函数来释放它。

于 2016-05-19T06:33:12.667 回答