我正在使用直接缓冲区 (java.nio) 来存储 JOGL 的顶点信息。这些缓冲区很大,并且在应用程序生命周期中会多次更换。内存没有及时释放,经过几次更换后内存不足。
似乎没有使用 java.nio 的缓冲区类解除分配的好方法。我的问题是这样的:
JOGL 中是否有一些方法可以删除直接缓冲区?我正在研究 glDeleteBuffer(),但似乎这只会从视频卡内存中删除缓冲区。
谢谢
我正在使用直接缓冲区 (java.nio) 来存储 JOGL 的顶点信息。这些缓冲区很大,并且在应用程序生命周期中会多次更换。内存没有及时释放,经过几次更换后内存不足。
似乎没有使用 java.nio 的缓冲区类解除分配的好方法。我的问题是这样的:
JOGL 中是否有一些方法可以删除直接缓冲区?我正在研究 glDeleteBuffer(),但似乎这只会从视频卡内存中删除缓冲区。
谢谢
直接 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 中有一个很好的例子。
直接缓冲区很棘手,并且没有通常的垃圾收集保证 - 请参阅更多详细信息: http: //docs.oracle.com/javase/7/docs/api/java/nio/ByteBuffer.html#direct
如果您遇到问题,我建议分配一次并重新使用缓冲区,而不是重复分配和解除分配。
完成释放的方式很糟糕——一个软引用基本上被插入到一个 Cleaner 对象中,然后当拥有 ByteBuffer 被垃圾回收时它会进行释放。但这并不能真正保证及时调用。
释放直接缓冲区是垃圾收集器在标记 ByteBuffer 对象一段时间后完成的工作。
您可以在删除对缓冲区的最后一个引用后立即尝试调用 gc。至少有可能内存会更快地释放。
使用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;
}
}
}
与其滥用非公共 API 的反射,您可以完全在受支持的公共 API 内轻松完成此操作。
编写一些用 NewDirectByteBuffer 包装 malloc 的 JNI(记得设置顺序),以及一个类似的函数来释放它。