在内存有限的系统上写入大文件时如何避免 mapFailed() 错误
How do I avoid mapFailed() error when writing to large file on system with limited memory
我刚刚在我的 opensrc 库代码中遇到一个错误,它分配了一个大缓冲区来修改一个大的 flac 文件,这个错误只发生在一台内存为 3Gb 的旧 PC 机器上,使用 Java 1.8 .0_74 25.74-b02 32 位
原来我只是分配一个缓冲区
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
当你分配缓冲区时,你基本上从你的操作系统中获得了一大块虚拟内存(这个虚拟内存是有限的,理论上限是你的 RAM + 配置的任何交换 - 其他程序首先获取的任何其他内容和 OS)
内存映射只是将 space 占用的磁盘文件添加到虚拟内存中(好吧,有一些开销,但不是那么多)- 所以你可以获得更多。
这些都不必经常出现在 RAM 中,它的一部分可以在任何给定时间换出到磁盘。
虽然映射缓冲区在任何一个时间点可能使用较少的物理内存,但它仍然需要可用(逻辑)地址 space 等于缓冲区的总(逻辑)大小。更糟的是,它可能(可能)要求地址 space 是连续的。无论出于何种原因,那台旧计算机似乎无法提供足够的额外逻辑地址 space。两个可能的解释是 (1) 有限的逻辑地址 space + 大量的缓冲内存需求,以及 (2) OS 对可以映射为文件的内存量施加的一些内部限制I/O.
关于第一种可能性,请考虑这样一个事实,即在虚拟内存系统中,每个进程都在其自己的逻辑地址中执行 space(因此可以访问完整的 2^32 字节寻址)。因此,如果——在您尝试实例化 MappedByteBuffer
的时间点——JVM 进程的当前大小加上 MappedByteBuffer
的总(逻辑)大小大于 2^32字节(~ 4 GB),然后你会 运行 到 OutOfMemoryError
(或者 class 选择代替它的任何 error/exception,例如 IOException: Map failed
) .
关于第二种可能性,最简单的评估方法可能是在您尝试实例化 MappedByteBuffer
时分析您的程序/JVM。如果 JVM 进程分配的内存 + 所需的 totalTargetSize
远低于 2^32 字节上限,但您仍然收到 "map failed" 错误,则可能是某些内部 OS 限制内存映射文件的大小是根本原因。
那么,就可能的解决方案而言,这意味着什么?
- 只是不要使用那台旧电脑。 (更可取,但可能不可行)
- 确保 JVM 中的其他所有内容在
MappedByteBuffer
的生命周期内占用的内存尽可能少。 (似是而非,但可能无关紧要且绝对不切实际)
- 将该文件分成更小的块,然后一次只对一个块进行操作。 (可能取决于文件的性质)
- 使用不同的/较小的缓冲区。 ...并忍受性能下降。 (这是最现实的解决方案,即使是最令人沮丧的)
此外,totalTargetSize
究竟是什么?
编辑:
经过一些挖掘,似乎很明显 IOException 是由于 running out of address space in a 32-bit environment. This can happen even when the file itself is under 2^32 bytes either due to the lack of sufficient contiguous address space, or due to other sufficiently large address space requirements in the JVM at the same time combined with the large MappedByteBuffer
request (see comments). To be clear, an IOE can still be thrown rather than an OOM even if the original cause is ENOMEM. Moreover, there appear to be issues with older [insert Microsoft OS here] 32-bit environments in particular (example, example)。
看来你有三个主要选择。
- 一共使用“the 64-bit JRE or...another operating system”。
- 使用不同类型的较小缓冲区并分块操作文件。 (并且由于不使用映射缓冲区而受到性能影响)
- 出于性能原因继续使用
MappedFileBuffer
,但也以较小的块对文件进行操作以解决地址 space 限制。
我将在较小的块中使用 MappedFileBuffer
作为第三个原因是因为在取消映射 MappedFileBuffer
(example), which is something you would necessarily have to do in between processing each chunk in order to avoid hitting the 32-bit ceiling due to the combined address space footprint of accumulated mappings. (NOTE: this only applies if it is the 32-bit address space ceiling and not some internal OS restrictions that are the problem... if the latter, then ignore this paragraph) You could attempt this strategy (delete all references then run the GC), but you would essentially be at the mercy of how the GC and your underlying OS interact regarding memory-mapped files. And other potential workarounds that attempt to manipulate the underlying memory-mapped file more-or-less directly (example) are exceedingly dangerous and specifically condemned by Oracle (see last paragraph). Finally, considering that GC behavior is unreliable anyway, and moreover that the official documentation explicitly states that "many of the details of memory-mapped files [are] unspecified" 时存在公认且未解决的问题,我会 不 推荐像这样使用MappedFileBuffer
,无论您可能阅读过任何解决方法。
因此,除非您愿意冒险,否则我建议您要么遵循 Oracle 的明确建议(第 1 点),要么使用不同的缓冲区类型将文件作为一系列较小的块处理(第 2 点)。
我刚刚在我的 opensrc 库代码中遇到一个错误,它分配了一个大缓冲区来修改一个大的 flac 文件,这个错误只发生在一台内存为 3Gb 的旧 PC 机器上,使用 Java 1.8 .0_74 25.74-b02 32 位
原来我只是分配一个缓冲区
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
当你分配缓冲区时,你基本上从你的操作系统中获得了一大块虚拟内存(这个虚拟内存是有限的,理论上限是你的 RAM + 配置的任何交换 - 其他程序首先获取的任何其他内容和 OS)
内存映射只是将 space 占用的磁盘文件添加到虚拟内存中(好吧,有一些开销,但不是那么多)- 所以你可以获得更多。
这些都不必经常出现在 RAM 中,它的一部分可以在任何给定时间换出到磁盘。
虽然映射缓冲区在任何一个时间点可能使用较少的物理内存,但它仍然需要可用(逻辑)地址 space 等于缓冲区的总(逻辑)大小。更糟的是,它可能(可能)要求地址 space 是连续的。无论出于何种原因,那台旧计算机似乎无法提供足够的额外逻辑地址 space。两个可能的解释是 (1) 有限的逻辑地址 space + 大量的缓冲内存需求,以及 (2) OS 对可以映射为文件的内存量施加的一些内部限制I/O.
关于第一种可能性,请考虑这样一个事实,即在虚拟内存系统中,每个进程都在其自己的逻辑地址中执行 space(因此可以访问完整的 2^32 字节寻址)。因此,如果——在您尝试实例化 MappedByteBuffer
的时间点——JVM 进程的当前大小加上 MappedByteBuffer
的总(逻辑)大小大于 2^32字节(~ 4 GB),然后你会 运行 到 OutOfMemoryError
(或者 class 选择代替它的任何 error/exception,例如 IOException: Map failed
) .
关于第二种可能性,最简单的评估方法可能是在您尝试实例化 MappedByteBuffer
时分析您的程序/JVM。如果 JVM 进程分配的内存 + 所需的 totalTargetSize
远低于 2^32 字节上限,但您仍然收到 "map failed" 错误,则可能是某些内部 OS 限制内存映射文件的大小是根本原因。
那么,就可能的解决方案而言,这意味着什么?
- 只是不要使用那台旧电脑。 (更可取,但可能不可行)
- 确保 JVM 中的其他所有内容在
MappedByteBuffer
的生命周期内占用的内存尽可能少。 (似是而非,但可能无关紧要且绝对不切实际) - 将该文件分成更小的块,然后一次只对一个块进行操作。 (可能取决于文件的性质)
- 使用不同的/较小的缓冲区。 ...并忍受性能下降。 (这是最现实的解决方案,即使是最令人沮丧的)
此外,totalTargetSize
究竟是什么?
编辑:
经过一些挖掘,似乎很明显 IOException 是由于 running out of address space in a 32-bit environment. This can happen even when the file itself is under 2^32 bytes either due to the lack of sufficient contiguous address space, or due to other sufficiently large address space requirements in the JVM at the same time combined with the large MappedByteBuffer
request (see comments). To be clear, an IOE can still be thrown rather than an OOM even if the original cause is ENOMEM. Moreover, there appear to be issues with older [insert Microsoft OS here] 32-bit environments in particular (example, example)。
看来你有三个主要选择。
- 一共使用“the 64-bit JRE or...another operating system”。
- 使用不同类型的较小缓冲区并分块操作文件。 (并且由于不使用映射缓冲区而受到性能影响)
- 出于性能原因继续使用
MappedFileBuffer
,但也以较小的块对文件进行操作以解决地址 space 限制。
我将在较小的块中使用 MappedFileBuffer
作为第三个原因是因为在取消映射 MappedFileBuffer
(example), which is something you would necessarily have to do in between processing each chunk in order to avoid hitting the 32-bit ceiling due to the combined address space footprint of accumulated mappings. (NOTE: this only applies if it is the 32-bit address space ceiling and not some internal OS restrictions that are the problem... if the latter, then ignore this paragraph) You could attempt this strategy (delete all references then run the GC), but you would essentially be at the mercy of how the GC and your underlying OS interact regarding memory-mapped files. And other potential workarounds that attempt to manipulate the underlying memory-mapped file more-or-less directly (example) are exceedingly dangerous and specifically condemned by Oracle (see last paragraph). Finally, considering that GC behavior is unreliable anyway, and moreover that the official documentation explicitly states that "many of the details of memory-mapped files [are] unspecified" 时存在公认且未解决的问题,我会 不 推荐像这样使用MappedFileBuffer
,无论您可能阅读过任何解决方法。
因此,除非您愿意冒险,否则我建议您要么遵循 Oracle 的明确建议(第 1 点),要么使用不同的缓冲区类型将文件作为一系列较小的块处理(第 2 点)。