java 堆内存不释放_关于Java: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

#include

void someOp(unsigned char* buf, int size){

unsigned char *foo;

foo = malloc(1000000);

if(!foo){

fprintf(stderr,"Failed to malloc 1000000 bytes of memory

");

return;

}

free(foo);

buf[0] = 100;

}

麻烦的是反复调用此函数后,Java堆有些稳定(增长缓慢),但是C函数最终无法分配更多的内存。在较高的层次上,我认为这是因为Java正在为C堆分配内存,但由于Java ByteBuffer对象相对较小,因此并未清除指向该内存的ByteBuffer。

到目前为止,我发现在我的函数中手动运行GC将提供所需的清除,但是这似乎既不明智,也不可行。

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

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

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

您实际上正面临Java VM中的一个已知错误。错误报告中列出的最佳解决方法是:

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

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

偶尔插入明确的System.gc()调用,以确保回收直接缓冲区。

减小年轻一代的大小,以强制使用更频繁的GC。

在应用程序级别显式池直接缓冲区。

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

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

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

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

我还将使用pmap工具(或Windows上的任何等效工具)来检查虚拟内存映射。您可能会通过分配可变大小的缓冲区来碎片化C堆。如果真是这样,那么您会看到每张更大的虚拟地图,其中" anon"块之间存在间隙。解决方案是分配大于所需大小的恒定大小的块。

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

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

您可以使用JNA的Native.getDirectBufferPointer(Buffer) [6]而不是编写诸如C的代码来从Java调用该函数。

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

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

我还不知道您是否可以释放Java ByteBuffer.allocateDirect(int) [8]创建的直接Buffer,其原因与上述完全相同。在分发新的直接Buffer时,是使用JVM还是进行1:1内存分配,可能是JVM或Java平台实现特定的详细信息。

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

*

* 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);

}

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

更好的解决方案是重用相同的ByteBuffer,因此您无需重新分配ByteBuffer。

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

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

为了找出问题所在,我将切换到(Java)堆分配的缓冲区(只需使用allocate方法代替allocateDirect。如果这使您的内存问题消失了,那么您已经找到了罪魁祸首)下一个问题是直接字节缓冲区在性能方面是否具有任何优势(如果没有)(我想它没有),那么您就不必担心如何正确清理它。

AFAIK通过JNA将ByteBuffer传递到本机代码的唯一方法是allocateDirect ...使用分配而不是allocateDirect时,我已经看到了一个错误。 这里的使用模式实际上是传递图像缓冲区,它们大约运行3000 * 2000字节。

那也是我所假设的,然后我仔细研究了JNA文档(jna.dev.java.net/#mapping),并且似乎可以使用常规Java数组,前提是您不必在外部使用该数组 本机函数调用。 但是,这可能会导致复制开销。

换句话说,跳过使用nio.ByteBuffer对象,只需传递byte[] ...值得一试。

是的,您还可以使用wrap来访问JNA接口的byte[],同时允许Java代码与ByteBuffer一起使用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值