JNA/ByteBuffer没有被释放,导致C堆内存耗尽。

11

首先,我要说明我的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堆分配内存,但由于Java ByteBuffer对象相对较小,没有清理指向该内存的ByteBuffer。

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

如何更好地管理此问题,以便适当释放ByteBuffer空间并控制我的C堆空间?

我的理解是否不正确(我是否运行有误)?

编辑:将缓冲区大小调整为更符合我的实际应用程序,我正在为大约3000x2000的图像分配内存...

5个回答

9
您实际上正在面对Java VM中已知的一个错误。报告中列出的最佳解决方法是:
  • “-XX:MaxDirectMemorySize =”选项可用于限制使用的直接内存量。尝试分配直接内存会导致超过此限制,从而引起完整的GC,以促进引用处理和释放未引用的缓冲区。
其他可能的解决方法包括:
  • 插入偶尔的显式System.gc()调用以确保回收直接缓冲区。
  • 减小年轻代的大小以强制更频繁的GC。
  • 在应用程序级别显式地池化直接缓冲区。
如果您真的想依赖直接字节缓冲区,那么我建议在应用程序级别进行池化。根据您的应用程序复杂性,您甚至可以简单地缓存并重复使用同一缓冲区(请注意多个线程)。

我正在使用-XX:MaxDirectMemorySize=128m,但是当我创建和丢弃太多的Memory实例而没有调用System.gc()时,仍然会出现OutOfMemoryError。有了它,错误就消失了。 - Mark Jeronimus

4
我认为你的诊断是正确的:你从未用尽Java堆,因此JVM不会进行垃圾回收,映射的缓冲区也没有被释放。手动运行GC时没有问题似乎证实了这一点。您还可以打开详细的收集日志作为辅助确认。
那么你能做什么呢?首先,我会尝试使用-Xms命令行参数将初始JVM堆大小保持较小。如果您的程序不断在Java堆上分配小量内存,则可能会出现问题,因为它会更频繁地运行GC。
我还会使用pmap工具(或Windows上等效的工具)来检查虚拟内存映射。你可能正在通过分配可变大小的缓冲区来分散C堆,如果是这种情况,那么您将看到一个越来越大的虚拟映射,并且“anon”块之间存在间隙。解决方案是分配大于所需的恒定大小块。

1

要释放Buffer[1]内存,您可以使用JNI

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

与其编写C代码来从Java调用该函数,您可以使用JNANative.getDirectBufferPointer(Buffer)[6]

之后唯一剩下的就是放弃对Buffer对象的所有引用。Java的垃圾回收器将像处理任何其他未被引用的对象一样释放Buffer实例。

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

我也不知道是否可以释放由Java的ByteBuffer.allocateDirect(int)[8]创建的直接Buffer,原因与上述相同。这可能取决于JVM或Java平台实现的具体细节,无论是使用池还是在分配新的直接Buffer时进行1:1内存分配。

以下是我的库中关于直接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

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

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

为了隔离问题,我建议切换到(Java)堆分配的缓冲区(只需使用allocate方法代替allocateDirect)。如果这样可以解决您的内存问题,那么您就找到了罪魁祸首。下一个问题将是直接字节缓冲区是否具有任何性能优势。如果没有(我猜测没有),那么您就不需要担心如何正确清理它。


1
据我所知,通过JNA将ByteBuffer传递到本地代码的唯一方法是使用allocateDirect...当使用allocate而不是allocateDirect时,我曾经看到过这样的错误。这里的使用模式实际上是在传递图像缓冲区,其大小约为3000*2000字节。 - Mark Elliot
1
这也是我最初的假设,然后我仔细查看了JNA文档(https://jna.dev.java.net/#mapping),发现你可以使用普通的Java数组,只要你不在本地函数调用之外保留该数组。但是,这可能会引入复制开销。 - kdgregory
换句话说,跳过使用 nio.ByteBuffer 对象,直接传递一个 byte[] 数组……值得一试。 - Mark Elliot
2
是的,您也可以使用“wrap”来访问JNA接口的“byte []”,同时允许Java代码使用“ByteBuffer”。 - erickson

1
如果你的堆内存不足,GC将自动触发。然而,如果你的直接内存不足,在Sun的JVM上至少是如此,GC不会被触发,即使GC可以释放足够的内存,你只会得到一个OutOfMemoryError。在这种情况下,我发现你必须手动触发GC。
一个更好的解决方案可能是重用相同的ByteBuffer,这样你就不需要重新分配ByteBuffers了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接