代码之家  ›  专栏  ›  技术社区  ›  Mark Elliot

JNA/ByteBuffer未释放,导致C堆内存不足

  •  11
  • Mark Elliot  · 技术社区  · 16 年前

    让我先说,我对JNA和Java直接原生内存分配的理解最多是内脏的,所以我试着描述我对正在发生的事情的理解。除了回应之外,任何修正都是很好的…

    我正在运行一个应用程序,它使用Java和C本地代码混合使用JNA和AM来运行一个可重复的问题,Java垃圾收集器不能自由引用直接的本地内存分配,导致C堆内存不足。

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

    这是一组有些琐碎的代码,展示了这个问题-- 实际上,我正试图在函数调用期间在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字节缓冲对象相对较小。

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

    如何更好地管理这个问题,以便适当地释放ByteBuffer空间和控制C堆空间?

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

    编辑 :调整缓冲区大小以更反映我的实际应用程序,我将为大约30000x2000个图像分配…

    5 回复  |  直到 13 年前
        1
  •  4
  •   kdgregory    16 年前

    我认为你已经正确地诊断了:你从来没有耗尽Java堆,所以JVM不垃圾收集,并且映射的缓冲区没有被释放。手动运行GC时没有问题的事实似乎证实了这一点。您还可以启用详细收集日志记录作为辅助确认。

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

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

        2
  •  8
  •   jbellis    15 年前

    你真的面对 a known bug in the Java VM . Bug报告中列出的最佳解决方法是:

    • “可以使用-xx:maxdirectmemorysize=选项限制使用的直接内存量。如果试图分配直接内存而导致超过此限制,将导致完全GC,从而引发引用处理和释放未引用的缓冲区。”

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

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

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

        3
  •  1
  •   erickson    16 年前

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

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

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

        4
  •  1
  •   Peter Lawrey    16 年前

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

    更好的解决方案可能是重用相同的bytebuffer,这样就不需要重新定位bytebuffers。

        5
  •  1
  •   Gima    13 年前

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

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

    而不是编写代码,例如C来调用Java中所说的函数,您可以使用 JNA Native.getDirectBufferPointer(Buffer) [ 6 ]

    之后唯一剩下的就是放弃所有对 缓冲区 对象。Java的垃圾回收将免费 缓冲区 与其他任何未引用对象相同。

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

    我也不知道你能不能给一个直拨电话 缓冲区 由Java创建 ByteBuffer.allocateDirect(int) [ 8 ] 原因与上述完全相同。它可以是JVM或Java平台实现的具体细节,无论它们使用池还是在分发新的直接内存时执行1:1内存分配。 缓冲区 S.

    下面是我的库中关于direct的一个稍微修改过的片段 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);
    }