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

我刚刚在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大文件上载抛出java.io.IOException:映射失败

完整的代码可以在这里看到

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

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

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

那么这意味着什么是尽可能的解决方案呢?

  1. 只是不要使用旧PC。 (最好,但可能不可行)
  2. 确保JVM中的其他所有内容在MappedByteBuffer的生命周期内具有尽可能低的内存占用。 (看似合理,但可能无关紧要,绝对不切实际)
  3. 将该文件分成较小的块,然后一次只对一个块进行操作。 (可能取决于文件的性质)
  4. 使用不同/较小的缓冲区。 ……只是忍受性能下降。 (这是最现实的解决方案,即使这是最令人沮丧的)

另外,问题案例的totalTargetSize究竟是什么?


编辑:

在进行一些挖掘后,似乎很清楚IOException是由于32位环境中的地址空间不足造成的 。 即使文件本身由于缺少足够的连续地址空间而导致文件本身低于2 ^ 32字节,或者由于JVM中的其他足够大的地址空间要求同时结合大型MappedByteBuffer请求,也会发生这种情况( 请参阅注释) )。 要清楚的是, 即使原始原因是ENOMEM ,仍然可以抛出IOE而不是OOM。 此外,旧的[在此处插入Microsoft OS]特别是32位环境( 例如 , 示例 )似乎存在问题。

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

  1. 完全使用“ 64位JRE或……另一个操作系统 ”。
  2. 使用不同类型的较小缓冲区并以块为单位对文件进行操作。 (并且由于不使用映射缓冲区而导致性能下降)
  3. 出于性能原因继续使用MappedFileBuffer ,但还要以较小的块操作文件,以解决地址空间限制。

我把MappedFileBuffer在较小的块中作为第三个的原因是因为在取消MappedFileBuffer ( 示例 )时已经建立并且未解决的问题,这是在处理每个块之间必须要做的事情,以避免命中32由于累积映射的组合地址空间占用而导致的上限。 (注意:这仅适用于32位地址空间上限而不是某些内部操作系统限制的问题……如果是后者,则忽略此段)您可以尝试此策略 (删除所有引用然后运行GC),但你基本上可以接受GC和底层操作系统如何在内存映射文件上进行交互。 尝试直接操作底层内存映射文件的其他潜在解决方法( 例如 )非常危险,并且特别受到Oracle的谴责( 参见上一段 )。 最后,考虑到GC行为无论如何都是不可靠的,而且官方文档明确指出“ 内存映射文件的许多细节 都未 指定 ”,我建议像这样使用MappedFileBuffer而不管你可能阅读的任何解决方法关于。

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

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

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

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