在 Java 中为 JOGL 释放直接缓冲区本机内存

Posted

技术标签:

【中文标题】在 Java 中为 JOGL 释放直接缓冲区本机内存【英文标题】:Deallocating Direct Buffer Native Memory in Java for JOGL 【发布时间】:2010-08-16 19:18:43 【问题描述】:

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

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

在 JOGL 中有什么方法可以删除 Direct Buffers 吗?我正在研究 glDeleteBuffer(),但似乎这只会从视频卡内存中删除缓冲区。

谢谢

【问题讨论】:

【参考方案1】:

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

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

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

编辑:My latest example 甚至可以使用 Java 1.9,并支持 OpenJDK、Oracle Java、Sun Java、Apache Harmony、GNU Classpath 和 android。您可能必须删除一些语法糖才能使其与 Java

参考: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

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

This solution 已集成在 Java 14 中:

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

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

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

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

N.B:sun.misc.Cleaner 已被移动到 Java 1.9 中的“java.base”模块中的jdk.internal.ref.Cleaner,后者实现了 java.lang.Runnable(感谢 Alan Bateman提醒我区别)很短的时间,但it's no longer the case。你必须调用 sun.misc.Unsafe.invokeCleaner(),它是 done in JogAmp's Gluegen。我更喜欢将 Cleaner 用作 Runnable,因为它避免了依赖 sun.misc.Unsafe,但它现在不起作用。

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

My very latest example 需要使用孵化功能(需要 Java >= 14)但非常简单。

a good example in Lucene 在更宽松的许可证下。

【讨论】:

+1:这实际上与JOGL 无关,我可以在这里找到关于释放 java nio 缓冲区的最佳参考 您列表中的最后一个解决方案可能还没有为 Java 9 做好准备。JEP 仍然是一个草案。 openjdk.java.net/jeps/191 @LukeHutchison sun.misc.Unsafe 包含名为 invokeCleaner(java.nio.ByteBuffer) 的方法,而 jdk.internal.misc.Unsafe 没有。因此,这并不完全是重命名。 我注意到一件事:在 JDK9+ 中,调用 sun.misc.Cleaner.clean() 会在 stderr 上发出反射(封装)警告。 Unsafe.theUnsafe.invokeCleaner(byteBuffer) 进行相同的调用,但不打印反射警告。 @LukeHutchison 您只需要在使用 MemorySegment.close() 时使用 attachment(),现在简单多了,但我仍然收到警告。【参考方案2】:

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

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

【讨论】:

你最后的建议很好。缓冲区在多次使用时非常有用。也可以切片。但是,通过保留未使用的已分配本机内存,存在增加运行时内存占用的风险。如果正确实施,直接 NIO 缓冲区的释放比试图避免重复分配和释放它们更有效且侵入性更小。【参考方案3】:

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

【讨论】:

你说它很糟糕,但 AFAIK 是尽你所能。直接缓冲区是有意从托管堆中分配出来的,所以我们不应该对它们的管理有点缺乏感到惊讶。无论如何,在抛出 OOM 之前,会尝试处理所有挂起的引用并运行 GC。它可能无法按设计工作。 +++ 恐怕,除非我们解决 Unsafe 并冒类似 malloc/free 的泄漏和崩溃的风险,否则我们只能得到这些。【参考方案4】:

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

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

【讨论】:

“调用 gc”是指 System.gc() 我猜;这确实是一个长镜头,因为不能保证这个电话真的能做任何事情。确保在调用之前设置对直接缓冲区= null 的任何引用,否则 GC 肯定会认为缓冲区仍在使用中。【参考方案5】:

使用gouessej's answer 中的信息,我能够将这个实用程序类放在一起以释放给定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;
        
    


【讨论】:

【参考方案6】:

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

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

【讨论】:

我同意 ochi 并查看我帖子中的最后一个建议,它不使用任何非公共 API。

以上是关于在 Java 中为 JOGL 释放直接缓冲区本机内存的主要内容,如果未能解决你的问题,请参考以下文章

在 jogl 中使用顶点缓冲区,当三角形太多时崩溃

Java-NIO:直接缓冲区与非直接缓冲区

JOGL 中顶点缓冲对象的问题

JOGL/OpenGL VBO - 如何渲染顶点?

Java NIO -- 直接缓冲区与非直接缓冲区

JOGL 和帧缓冲区渲染到纹理的问题:无效帧缓冲区操作错误