直接缓冲存储器

Direct buffer memory

我需要从 Web 请求中 return 一个相当大的文件。该文件的大小约为 670mb。在大多数情况下,这会正常工作,但一段时间后会抛出以下错误:

java.lang.OutOfMemoryError: Direct buffer memory
    at java.nio.Bits.reserveMemory(Bits.java:694) ~[na:1.8.0_162]
    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) ~[na:1.8.0_162]
    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) ~[na:1.8.0_162]
    at sun.nio.ch.Util.getTemporaryDirectBuffer(Util.java:241) ~[na:1.8.0_162]
    at sun.nio.ch.IOUtil.read(IOUtil.java:195) ~[na:1.8.0_162]
    at sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:159) ~[na:1.8.0_162]
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65) ~[na:1.8.0_162]
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109) ~[na:1.8.0_162]
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103) ~[na:1.8.0_162]
    at java.nio.file.Files.read(Files.java:3105) ~[na:1.8.0_162]
    at java.nio.file.Files.readAllBytes(Files.java:3158) ~[na:1.8.0_162]

我已将堆大小设置为 4096mb,我认为这应该足以处理此类文件。此外,当发生此错误时,我使用 jmap 进行了堆转储来分析当前状态。发现两个比较大的byte[],应该是我要的文件return。但是堆的大小只有 1.6gb 左右,不接近配置的 4gb。

根据类似问题中的一些其他答案 (),我在 returning 此文件之前尝试了 运行 手动 gc。问题仍然存在,但现在只是零星的。一段时间后问题出现了,但是当我再次厌倦 运行 相同的请求时,垃圾收集似乎解决了导致问题的任何问题,但这还不够,因为问题显然仍然会发生。还有其他方法可以避免这种内存问题吗?

DirectByteBuffer 管理的实际内存缓冲区未在堆中分配。它们是使用分配“本机内存”的 Unsafe.allocateMemory 分配的。所以增加或减少堆大小都无济于事。

当 GC 检测到不再引用 DirectByteBuffer 时,将使用 Cleaner 释放本机内存。然而,这发生在 post-collection 阶段,所以如果直接缓冲区的需求/周转率太大,收集器可能无法跟上。如果发生这种情况,您将获得 OOME。


你能做些什么?

AFAIK,您唯一能做的就是强制更频繁地进行垃圾回收。但这可能会对性能产生影响。而且我认为这不是一个有保证的解决方案。

真正的解决方案是采用不同的方法。

您看到您正在从网络服务器提供大量非常大的文件,并且堆栈跟踪显示您正在使用 Files::readAllBytes 将它们加载到内存中,然后(大概)使用单个write。据推测,您这样做是为了尽可能获得最快的下载时间。这是一个错误:

  • 您正在占用大量内存(是垃圾收集器的倍数并给垃圾收集器带来压力。这会导致更多的 GC 运行和偶尔的 OOME。它还可能以各种方式影响服务器上的其他应用程序方式。

  • 传输文件的瓶颈可能不是从磁盘读取数据的过程。 (真正的瓶颈是通常通过网络通过 TCP 流发送数据,或者将其写入客户端的文件系统。)

  • 如果您按顺序读取大文件,现代 Linux OS 通常会使用预读多个磁盘块并将这些块保存在 (OS) 缓冲区缓存。这将减少应用程序进行的 read 系统调用的延迟。

因此,对于这种大小的文件,更好的办法是流式传输文件。分配一个大的(几兆字节)ByteBuffer 并在循环中读/写, 使用 Files::copy(...) 复制文件(javadoc ) 应该为您处理缓冲。

(也可以选择使用映射到 Linux sendfile 系统调用的东西。这会将数据从一个文件描述符复制到另一个文件描述符,而不会将其写入用户-space缓冲区。)

您也可以尝试使用 JVM 选项 -XX:MaxDirectMemorySize 增加用于 DirectByteBuffer 的缓冲区大小。 Java 文档对这个参数不是很详细,但是根据这个 page 它将默认设置为 64MB,除非你指定了 -Xmx 标志。因此,如果您没有设置此标志,则分配的缓冲区可能太小。或者,如果您有一个非常大的文件并设置了 -Xmx,派生的 2GB 可能太小,您仍然可以通过手动设置更大的缓冲区来获益。

总而言之,更好的方法可能是按照 Stephen C 的建议流式传输文件。