使用 java.nio.MappedByteBuffer 时防止 OutOfMemory

Posted

技术标签:

【中文标题】使用 java.nio.MappedByteBuffer 时防止 OutOfMemory【英文标题】:Prevent OutOfMemory when using java.nio.MappedByteBuffer 【发布时间】:2011-12-18 16:41:35 【问题描述】:

考虑创建 5-6 个线程的应用程序,每个线程在循环中为 5mb 页面大小分配 MappedByteBuffer。

MappedByteBuffer b = ch.map(FileChannel.MapMode.READ_ONLY, r, 1024*1024*5);

迟早,当应用程序处理大文件时,会抛出 oom

java.io.IOException: Map failed  at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:758)
Caused by: java.lang.OutOfMemoryError: Map failed
        at sun.nio.ch.FileChannelImpl.map0(Native Method)
        at sun.nio.ch.FileChannelImpl.map(FileChannelImpl.java:755)

根据规范,MappedBuffer 应该在它本身被 GC 时立即处理直接内存。看起来问题是,MappedBuffer-s 被 GC 处理得太晚了,然后直接内存就完成了。

如何避免这种情况?可能会说 MappedBuffer 隐式处理或使用某种 MappedBuffer 池

【问题讨论】:

好奇,你的代码在做什么? 异常是 NOT OOM 而是 IOException。您的虚拟地址空间不足。显示更多代码。 java中的映射缓冲区和回收是一个长期的问题(仍然没有优雅地解决) 【参考方案1】:

您可以通过直接清理映射的字节缓冲区来避免触发 GC。

public static void clean(ByteBuffer bb) 
    if(bb == null) return;
    Cleaner cleaner = ((DirectBuffer) bb).cleaner();
    if(cleaner != null) cleaner.clean();

如果你在丢弃之前调用它,你就不会耗尽虚拟内存。

也许您可以考虑减少创建更大的 ByteBuffers(除非您有大量文件)创建 MappedByteBuffer 不是免费的(在某些机器上大约需要 50 微秒)

【讨论】:

不幸的是,这些方法似乎做了一些事情,但它们实际上并没有像这样取消映射 ByteBuffer。此方法仅适用于直接内存。【参考方案2】:

错误消息显示“映射失败”,而不是“堆空间”或“永久空间”。这意味着 JVM 没有足够的地址空间可用。

参见 Sun 数据库中的 this bug,以及 this question。

第一个链接提供了一个解决方法 (ewww),它与第二个链接所说的很接近:

    try 
        buffer = channel.map(READ_ONLY, ofs, n);
     catch (java.io.IOException e) 
        System.gc();
        System.runFinalization();
        buffer = channel.map(READ_ONLY, ofs, n);
    

【讨论】:

根据 GC 算法及其参数,System.gc() 调用可能是 NOP。使用-XX:+DisableExplicitGC 有效去除了catch the OOM and retrying after GC request的丑陋补丁【参考方案3】:

也许一个WeakHashMap 来汇集那些MappedBuffers 会起作用。

但在您猜测根本原因之前,我建议您将您的应用程序连接到 Visual VM 1.3.3,并安装所有插件,这样您就可以准确了解导致 OOM 错误的原因。您假设这些 MappedBuffers 正在这样做,但对于 5-6 个线程,它们每个只有 5MB - 总共 25-30MB。

拥有数据总比猜测好。 Visual VM 会帮你搞定的。

【讨论】:

实际上,每个线程可以循环创建大量缓冲区,具体取决于文件大小。这意味着缓冲区的总量不受限制。可能我应该使用某种信号量来防止分配大量内存。无论如何,我会尝试分析器,你建议 映射的缓冲区不能被池化!创建它们是为了覆盖由映射虚拟地址返回的地址到文件中的某个位置,并通过页面错误加载数据+在文件句柄关闭或请求时写回。 ...和WeakHashMap 完全没用/有害,因为密钥不是缓冲区本身,因此在WeakHashMap 被删除之前它甚至都不是可用的GC(内部)。使用 GC 依赖结构来解决问题肯定是错误的。 有趣的是,bestsss 对每个答案都发表了评论,但他自己却什么也没提供。让我们看看你对这个问题的回答。 我会回答,我要求提供更多代码。目前提供的没有解决方案。它耗尽了虚拟内存并且映射可能不会成功。这是一项艰巨的任务,通常需要进行设计更改和提前思考。您可以在问题下方阅读我的评论。您的回答没有冒犯,但任何与 GC 相关的解决方案都必然比错误本身更糟糕。 GC 只是无法回收虚拟内存(通过足够快地垃圾地址的所有视图)并且如果显式 GC 被禁用......【参考方案4】:

MappedBuffer 本身一旦被 GC 就应该释放直接内存

它实际上并没有在我能看到的任何地方这么说。有一个长期存在的 Bug Parade 项目说它从未发布过。

确实是这样说的:

因此建议主要分配直接缓冲区 用于大型、长寿命的缓冲区

【讨论】:

它们肯定被处理掉了,主要问题是一个微小的 java 对象持有对兆字节的引用,而 GC 并不认为释放和最终确定/取消映射虚拟内存有任何紧迫性。 @bestsss 他们要么被“处置”,要么没有。下定决心。您需要查找bugs.sun.com/bugdatabase/… Parade item #4724038。它与您不符。 再次阅读评论。它们已被处理,但往往延迟太多。我非常了解 Windows 和 Linux/Solaris 上的错误和本机 impl。该错误并没有告诉映射的缓冲区没有未映射,它们是在 ByteBuffer(及其视图)被垃圾收集之后。它们只是不能通过正常方式强制关闭/取消映射。总是有可能破解和取消映射 Buffer 的风险 SIGSEV 或使用另一个文件,或任何未定义的结果。 该错误没有简单的解决方案 b/c 地址不能原子地取消映射(+flush)然后重新映射到 NUL 设备。提供此类功能的操作系统可以解决问题。添加同步会降低性能。如果操作系统提供 m_null_unmap 或类似的东西,它就可以了。就是这样:标记不属于进程的页面,刷新它们并将其映射回空设备。任何并发访问都会导致页面错误,因此操作系统可以处理它。

以上是关于使用 java.nio.MappedByteBuffer 时防止 OutOfMemory的主要内容,如果未能解决你的问题,请参考以下文章

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

今目标使用教程 今目标任务使用篇

Qt静态编译时使用OpenSSL有三种方式(不使用,动态使用,静态使用,默认是动态使用)

MySQL db 在按日期排序时使用“使用位置;使用临时;使用文件排序”

使用“使用严格”作为“使用强”的备份

Kettle java脚本组件的使用说明(简单使用升级使用)