问题 JNA / ByteBuffer没有被释放并导致C堆耗尽内存


首先我要说的是,我对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 ...


9107
2017-11-16 20:05


起源



答案:


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

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

我也用了 PMAP 工具(或其在Windows上的等价物)来检查虚拟内存映射。通过分配可变大小的缓冲区,您可能会分割C堆。如果是这种情况,那么你会看到一个更大的虚拟地图,“anon”块之间有间隙。并且解决方案是分配比您需要的更大的常量块。


4
2017-11-16 20:21





你真的面对了 Java VM中的已知错误。错误报告中列出的最佳解决方法是:

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

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

  • 插入偶然的显式System.gc()调用以确保回收直接缓冲区。
  • 减少年轻一代的规模,迫使更频繁的GC。
  • 在应用程序级别显式池化直接缓冲区。

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


8
2017-11-21 13:50



我在用着 -XX:MaxDirectMemorySize=128m 但我还是得到了 OutOfMemoryError 当我创造和丢弃太多 Memory 实例,没有调用 System.gc()。有了它,错误就消失了。 - Mark Jeronimus


答案:


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

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

我也用了 PMAP 工具(或其在Windows上的等价物)来检查虚拟内存映射。通过分配可变大小的缓冲区,您可能会分割C堆。如果是这种情况,那么你会看到一个更大的虚拟地图,“anon”块之间有间隙。并且解决方案是分配比您需要的更大的常量块。


4
2017-11-16 20:21





你真的面对了 Java VM中的已知错误。错误报告中列出的最佳解决方法是:

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

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

  • 插入偶然的显式System.gc()调用以确保回收直接缓冲区。
  • 减少年轻一代的规模,迫使更频繁的GC。
  • 在应用程序级别显式池化直接缓冲区。

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


8
2017-11-21 13:50



我在用着 -XX:MaxDirectMemorySize=128m 但我还是得到了 OutOfMemoryError 当我创造和丢弃太多 Memory 实例,没有调用 System.gc()。有了它,错误就消失了。 - Mark Jeronimus


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

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

为了隔离问题,我将切换到(Java)堆分配的缓冲区(只需使用 allocate 代替的方法 allocateDirect。如果这让你的记忆问题消失了,你就找到了罪魁祸首。接下来的问题是a 直接 字节缓冲区在性能方面具有任何优势。如果没有(我猜它没有),那么你不必担心如何正确清理它。


1
2017-11-16 20:19



AFAIK通过的唯一途径 ByteBuffer 通过JNA到本机代码是 allocateDirect...我在使用allocate而不是使用时看到的错误 allocateDirect。这里的使用模式实际上是传递图像缓冲区,运行大约3000 * 2000字节。 - Mark Elliot
这也是我的假设,然后我仔细研究了JNA文档(jna.dev.java.net/#mapping并且看起来你可以使用常规Java数组,前提是你不能在本机函数调用之外保留该数组。但是,这可能会引入复制开销。 - kdgregory
换句话说,跳过使用 nio.ByteBuffer 对象并简单地传递一个 byte[]...值得一试。 - Mark Elliot
是的,你也可以使用 wrap 提供访问权限 byte[] 用于JNA接口,同时允许Java代码与之配合使用 ByteBuffer。 - erickson


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

更好的解决方案可能是重用相同的ByteBuffer,因此您永远不需要重新定位ByteBuffers。


1
2017-11-21 14:38





直接自由 Buffer[1] 记忆,你可以使用 JNI

功能 GetDirectBufferAddress(JNIEnv* env, jobject buf)[3] 从 JNI 6 API 可用于获取指向内存的指针 Buffer 然后是标准 free(void *ptr) 命令指针释放内存。

而不是编写诸如C之类的代码来从Java调用所述函数,您可以使用 JNANative.getDirectBufferPointer(Buffer)[6]

之后唯一剩下的就是放弃所有引用 Buffer 目的。 Java的垃圾收集将释放 Buffer 实例,与任何其他未引用的对象一样。

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

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

以下是我的库中有关直接的略微修改的片段 ByteBuffer[9] 处理(使用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);
}

1
2017-07-03 22:00