8

我刚刚在我的 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: Map failed

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

4

2 回答 2

2

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

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

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

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

  1. 只是不要使用那台旧电脑。 (最好,但可能不可行)
  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)时存在公认且未解决的问题,这是您在处理每个块之间必须要做的事情,以避免达到 32 位由于累积映射的组合地址空间占用空间而导致的上限。 (注意:这仅适用于 32 位地址空间上限而不是一些内部操作系统限制问题...如果是后者,则忽略本段) 您可以尝试此策略MappedFileBuffer(删除所有引用然后运行 ​​GC),但您基本上将受制于 GC 和您的底层操作系统如何在内存映射文件方面进行交互。其他试图或多或少直接操作底层内存映射文件的潜在变通方法(示例)非常危险,并受到 Oracle 的特别谴责(参见最后一段)。最后,考虑到 GC 行为无论如何都是不可靠的,而且官方文档明确指出“内存映射文件的许多细节 [are] unspecified ”,我建议您使用MappedFileBuffer这样的解决方法,无论您可能读到什么解决方法.

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

于 2016-12-06T05:35:14.673 回答
1

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

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

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

于 2016-12-05T17:33:03.273 回答