JNA/ByteBuffer 没有被释放并导致 C 堆内存不足

Posted

技术标签:

【中文标题】JNA/ByteBuffer 没有被释放并导致 C 堆内存不足【英文标题】:JNA/ByteBuffer not getting freed and causing C heap to run out of memory 【发布时间】:2009-11-16 20:05:38 【问题描述】:

首先让我说,我对 JNA 和 Java 如何直接分配本机内存的理解充其量是发自内心的,所以我试图描述我对正在发生的事情的理解。除了响应之外的任何更正都会很棒......

我正在运行一个使用 JNA 混合 Java 和 C 本机代码的应用程序,并且正在运行 Java 垃圾收集器无法释放对直接本机内存分配的引用,从而导致 C 堆内存不足的可重现问题。

我确信我的 C 应用程序不是分配问题的根源,因为我将 java.nio.ByteBuffer 传递到我的 C 代码中,修改缓冲区,然后在我的 Java 函数中访问结果。在每个函数调用期间,我都有一个 malloc 和一个对应的 free,但是在 Java 中反复运行代码后,malloc 最终会失败。

这里有一组有点琐碎的代码展示了这个问题——实际上我试图在函数调用期间在 C 堆上分配大约 16-32MB 的空间

我的 Java 代码执行以下操作:

public class MyClass
    public void myfunction()
        ByteBuffer foo = ByteBuffer.allocateDirect(1000000);
        MyDirectAccessLib.someOp(foo, 1000000);
        System.out.println(foo.get(0));
    


public MyDirectAccessLib
    static 
        Native.register("libsomelibrary");
    
    public static native void someOp(ByteBuffer buf, int size);

那么我的 C 代码可能是这样的:

#include <stdio.h>
#include <stdlib.h>
void someOp(unsigned char* buf, int size)
    unsigned char *foo;
    foo = malloc(1000000);
    if(!foo)
        fprintf(stderr, "Failed to malloc 1000000 bytes of memory\n");
        return;
    
    free(foo);

    buf[0] = 100;

问题是在反复调用这个函数后,Java 堆有点稳定(增长缓慢),但 C 函数最终无法分配更多内存。概括地说,我认为这是因为 Java 正在为 C 堆分配内存,但没有清理指向该内存的 ByteBuffer,因为 Java ByteBuffer 对象相对较小。

到目前为止,我发现在我的函数中手动运行 GC 将提供所需的清理,但这似乎既是一个糟糕的主意,也是一个糟糕的解决方案。

我怎样才能更好地管理这个问题,以便适当地释放 ByteBuffer 空间并控制我的 C 堆空间?

我对问题的理解是否不正确(是不是我运行不正确)?

编辑:调整缓冲区大小以更好地反映我的实际应用程序,我正在为大约 3000x2000 的图像分配...

【问题讨论】:

【参考方案1】:

你实际上面对的是a known bug in the Java VM。错误报告中列出的最佳解决方法是:

“-XX:MaxDirectMemorySize= 选项可用于限制使用的直接内存量。尝试分配直接内存会导致超出此限制会导致完全 GC,从而引发引用处理和释放未引用的缓冲区。”

其他可能的解决方法包括:

偶尔插入显式 System.gc() 调用以确保回收直接缓冲区。 减小年轻代的大小以强制更频繁的 GC。 在应用程序级别显式池化直接缓冲区。

如果您真的想依赖直接字节缓冲区,那么我建议在应用程序级别进行池化。根据应用程序的复杂性,您甚至可以简单地缓存和重用同一个缓冲区(注意多个线程)。

【讨论】:

我正在使用-XX:MaxDirectMemorySize=128m,但是当我创建和丢弃太多Memory 实例时,我仍然得到OutOfMemoryError,而没有调用System.gc()。有了它,错误就消失了。【参考方案2】:

我认为您的诊断正确:您永远不会用完 Java 堆,因此 JVM 不会进行垃圾收集,并且映射的缓冲区也不会被释放。手动运行 GC 时没有问题的事实似乎证实了这一点。您还可以打开详细收集日志记录作为辅助确认。

那你能做什么?嗯,我要尝试的第一件事是使用 -Xms 命令行参数来保持初始 JVM 堆大小较小。如果您的程序不断在 Java 堆上分配少量内存,这可能会导致问题,因为它会更频繁地运行 GC。

我还会使用 pmap 工具(或 Windows 上的任何等效工具)来检查虚拟内存映射。通过分配可变大小的缓冲区,您可能正在对 C 堆进行分段。如果是这样的话,那么你会看到一个更大的虚拟地图,“anon”块之间有间隙。解决方案是分配比您需要的更大的固定大小的块。

【讨论】:

【参考方案3】:

我怀疑您的问题是由于使用了 direct 字节缓冲区。它们可以分配在 Java 堆之外。

如果您经常调用该方法,并且每次都分配小缓冲区,那么您的使用模式可能不适合直接缓冲区。

为了隔离问题,我会切换到(Java)堆分配的缓冲区(只需使用allocate 方法代替allocateDirect。如果这使您的内存问题消失了,那么您已经找到了罪魁祸首。下一个问题是 direct 字节缓冲区在性能方面是否有任何优势。如果没有(我猜它没有),那么您无需担心关于如何正确清理它。

【讨论】:

AFAIK 通过 JNA 将 ByteBuffer 传递到本机代码的唯一方法是 allocateDirect...我在使用 allocate 而不是 allocateDirect 时看到的错误。这里的使用模式实际上是传递图像缓冲区,大约 3000*2000 字节。 这也是我的假设,然后我仔细查看了 JNA 文档 (jna.dev.java.net/#mapping),看来您可以使用常规 Java 数组,只要您不坚持本机函数调用之外的那个数组。但是,这可能会引入复制开销。 换句话说,跳过使用nio.ByteBuffer 对象并简单地传递byte[]...值得一试。 是的,您还可以使用wrap 为JNA 接口提供对byte[] 的访问权限,同时允许Java 代码使用ByteBuffer【参考方案4】:

如果堆内存用完,会自动触发 GC。但是,如果直接内存用完,则不会触发 GC(至少在 Sun 的 JVM 上),即使 GC 会释放足够的内存,您也会收到 OutOfMemoryError。我发现在这种情况下你必须手动触发 GC。

更好的解决方案可能是重用相同的 ByteBuffer,这样您就不需要重新分配 ByteBuffer。

【讨论】:

【参考方案5】:

要直接释放Buffer[1]内存,可以使用JNI。

JNI 6 API 中的函数GetDirectBufferAddress(JNIEnv* env, jobject buf)[3] 可用于为Buffer 获取指向内存的指针,然后在指针上使用标准的free(void *ptr) 命令释放内存。

您可以使用JNA的Native.getDirectBufferPointer(Buffer)[6]

,而不是编写诸如 C 之类的代码来调用该函数

唯一剩下的就是放弃对Buffer 对象的所有引用。然后,Java 的垃圾收集将释放 Buffer 实例,就像它处理任何其他非引用对象一样。

请注意,直接Buffer 不一定将 1:1 映射到分配的内存区域。例如,JNI API 有 NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity)[7]。因此,您应该只释放Buffer 的内存,您知道其内存分配区域与本机内存是一对一的。

我也不知道您是否可以释放由 Java 的 ByteBuffer.allocateDirect(int)[8] 创建的直接 Buffer,原因与上述完全相同。在分发新的直接Buffers 时,它们是否使用池或执行 1:1 内存分配可能是 JVM 或 Java 平台实现的具体细节。

下面是我的库中关于直接ByteBuffer[9] 处理的稍微修改的 sn-p(使用 JNA Native[10]Pointer[11] 类):

/**
 * Allocate native memory and associate direct @link ByteBuffer with it.
 * 
 * @param bytes - How many bytes of memory to allocate for the buffer
 * @return The created @link ByteBuffer.
 */
public static ByteBuffer allocateByteBuffer(int bytes) 
        long lPtr = Native.malloc(bytes);
        if (lPtr == 0) throw new Error(
            "Failed to allocate direct byte buffer memory");
        return Native.getDirectByteBuffer(lPtr, bytes);


/**
 * Free native memory inside @link Buffer.
 * <p>
 * Use only buffers whose memory region you know to match one to one
 * with that of the underlying allocated memory region.
 * 
 * @param buffer - Buffer whose native memory is to be freed.
 * The class instance will remain. Don't use it anymore.
 */
public static void freeNativeBufferMemory(Buffer buffer) 
        buffer.clear();
        Pointer javaPointer = Native.getDirectBufferPointer(buffer);
        long lPtr = Pointer.nativeValue(javaPointer);
        Native.free(lPtr);

【讨论】:

以上是关于JNA/ByteBuffer 没有被释放并导致 C 堆内存不足的主要内容,如果未能解决你的问题,请参考以下文章

java在循环语句的执行语句赋予变量值循环结束后会释放吗?

Objective C 方法 removeFromSuperview 导致消息被发送到释放的对象

Linux中大文件日志删除,但空间未被释放-解决方案

Linux C遇到的常见错误

为啥我的自动释放对象没有被释放?

c++的程序结束后,还有可能一部分数据留在物理内存,没有被释放吗?