在内存有限的系统上写入大文件时如何避免 mapFailed() 错误

Posted

技术标签:

【中文标题】在内存有限的系统上写入大文件时如何避免 mapFailed() 错误【英文标题】:How do I avoid mapFailed() error when writing to large file on system with limited memory 【发布时间】:2016-11-11 10:11:44 【问题描述】:

我刚刚在我的 opensrc 库代码中遇到一个错误,该代码分配了一个大缓冲区来修改一个大的 flac 文件,该错误只发生在使用 Java 1.8.0_74 25.74-b02 32bit 的具有 3Gb 内存的旧 PC 机器上

原来我只是分配一个缓冲区

ByteBuffer audioData = ByteBuffer.allocateDirect((int)(fc.size() - fc.position()));

但有一段时间我把它当作

MappedByteBuffer mappedFile = fc.map(MapMode.READ_WRITE, 0, totalTargetSize);

我的(错误)理解是映射缓冲区比直接缓冲区使用更少的内存,因为整个映射缓冲区不必同时在内存中,只有正在使用的部分。但是这个答案说使用映射字节缓冲区是一个坏主意,所以我不清楚它是如何工作的

Java Large File Upload throws java.io.IOException: Map failed

完整代码见here

【问题讨论】:

它失败了,因为它无法在 32 位上分配那么多 地址 空间。这与物理 RAM 不足无关。 但是文件大小只有200mb,应该没问题 你不能确定,它需要一个这样大小的连续块。 啊,好吧,你确定从那个错误中知道它是地址空间错误还是如果实际上没有足够的可用内存,你会得到同样的错误 因为mmap 应该只是重新映射已经加载到系统缓存中的内存页面,所以这不太可能。 【参考方案1】:

尽管映射缓冲区可能在任何时间点使用较少的物理内存,但它仍然需要一个可用的(逻辑)地址空间,该地址空间等于缓冲区的总(逻辑)大小。更糟糕的是,它可能(可能)要求地址空间是连续的。无论出于何种原因,那台旧计算机似乎无法提供足够的额外逻辑地址空间。两种可能的解释是 (1) 有限的逻辑地址空间 + 大量的缓冲内存需求,以及 (2) 操作系统对可映射为 I/O 文件的内存量施加的一些内部限制。

关于第一种可能性,请考虑以下事实:在虚拟内存系统中,每个进程都在其自己的逻辑地址空间中执行(因此可以访问完整的 2^32 字节的寻址)。所以如果——在你尝试实例化MappedByteBuffer的时间点——JVM进程的当前大小加上MappedByteBuffer的总(逻辑)大小大于2^32字节(~4 GB),那么您将遇到OutOfMemoryError(或该类选择抛出的任何错误/异常,例如IOException: Map failed)。

关于第二种可能性,可能最简单的评估方法是在您尝试实例化MappedByteBuffer 时分析您的程序/JVM。如果 JVM 进程分配的内存 + 所需的 totalTargetSize 远低于 2^32 字节上限,但您仍然收到“映射失败”错误,那么很可能是某些内部操作系统对内存映射大小的限制文件是根本原因。

那么,尽可能解决方案意味着什么?

    只是不要使用那台旧电脑。 (最好,但可能不可行) 确保在MappedByteBuffer 的生命周期内,JVM 中的所有其他内容都具有尽可能低的内存占用。 (似是而非,但可能不相关且绝对不切实际) 将该文件分成更小的块,然后一次只对一个块进行操作。 (可能取决于文件的性质) 使用不同的/更小的缓冲区。 ...并忍受性能下降。 (这是最现实的解决方案,即使它最令人沮丧)

另外,对于您的问题案例,totalTargetSize 到底是什么?


编辑:

经过一番挖掘,IOException 显然是由running out of address space in a 32-bit environment 引起的。即使由于缺少足够的连续地址空间,或者由于同时JVM中其他足够大的地址空间要求,文件本身小于2^32字节,也会发生这种情况结合MappedByteBuffer请求(see comments)。需要明确的是,仍然可以抛出 IOE 而不是 OOM even if the original cause is ENOMEM。此外,尤其是较旧的 [在此处插入 Microsoft 操作系统] 32 位环境似乎存在问题(example、example)。

所以看起来你有三个主要选择。

    完全使用“the 64-bit JRE or...another operating system”。 使用不同类型的较小缓冲区并以块的形式对文件进行操作。 (并且由于不使用映射缓冲区而受到性能影响) 出于性能原因继续使用MappedFileBuffer,但也要以较小的块对文件进行操作以解决地址空间限制。

我之所以将MappedFileBuffer 放在较小的块中作为第三个原因是因为在取消映射MappedFileBuffer (example) 时存在公认且未解决的问题,这是您在处理之间必须要做的事情每个块,以避免由于累积映射的组合地址空间占用而达到 32 位上限。 (注意:这仅适用于 32 位地址空间上限而不是某些内部操作系统限制问题...如果是后者,则忽略此段) 您可以尝试 @987654328 @ (删除所有引用然后运行 ​​GC),但您基本上将受制于 GC 和您的底层操作系统如何在内存映射文件方面进行交互。其他试图或多或少直接操作底层内存映射文件的潜在解决方法 (example) 非常危险,并受到 Oracle (see last paragraph) 的特别谴责。最后,考虑到 GC 行为无论如何都是不可靠的,而且官方文档明确指出“many of the details of memory-mapped files [are] unspecified”,我会建议像这样使用MappedFileBuffer,无论您可能阅读到任何解决方法.

因此,除非您愿意冒险,否则我建议您遵循 Oracle 的明确建议(第 1 点),或者使用不同的缓冲区类型将文件作为一系列较小的块处理(第 2 点)。

【讨论】:

感谢我所做的解决方案是 4>,我之前创建的映射缓冲区的大小约为 200mb @PaulTaylor 告诉我进展如何。请参阅我的编辑(第二部分)以获取有关该问题的详细信息 + 参考。此外,如果您有足够的兴趣尝试诊断问题而不是解决问题,请参阅第一部分中的第三段(已编辑)。 好的,谢谢,我不能只使用 64bit,因为我需要继续支持 32bit,我会继续分块使用 bytebuffer,忘记使用 MappedByteBuffer。【参考方案2】:

当您分配缓冲区时,您基本上会从您的操作系统中获取大量虚拟内存(并且这个虚拟内存是有限的,理论上上限是您的 RAM + 配置的任何交换 - 其他程序和操作系统首先获取的任何其他内容)

内存映射只是将磁盘文件上占用的空间添加到虚拟内存(好吧,有一些开销,但不是那么多) - 所以你可以得到更多。

这些都不必经常存在于 RAM 中,它的一部分可以在任何给定时间换出到磁盘。

【讨论】:

以上是关于在内存有限的系统上写入大文件时如何避免 mapFailed() 错误的主要内容,如果未能解决你的问题,请参考以下文章

java 导出 excel 最佳实践,大文件 excel 避免OOM(内存溢出) 框架-02-API

如何锁定文件并避免在写入时读取

在 UNIX/Linux 中如何避免文件重叠?

在csv文件中写入字符串时,如何避免重复字符串?

CreateFileMapping,MapViewOfFile,如何避免占用系统内存

为啥堆栈内存大小如此有限?