【问题标题】:Direct buffer memory直接缓冲存储器
【发布时间】:2020-04-25 02:54:07
【问题描述】:

我需要从网络请求中返回一个相当大的文件。该文件大小约为 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[],应该是我要返回的文件。但是堆的大小只有 1.6gb 左右,而不是接近配置的 4gb。

根据类似问题中的其他答案 (https://stackoverflow.com/a/39984276/5126654),我尝试在返回此文件之前运行手动 gc。问题仍然存在,但现在只是零星的。一段时间后问题出现了,但是当我厌倦了再次运行相同的请求时,似乎垃圾收集处理了导致问题的任何原因,但这还不够,因为问题显然仍然可能发生。有没有其他方法可以避免这个内存问题?

【问题讨论】:

  • 不要使用readAllBytes()。它需要一个文件大小的缓冲区。 TCP/IP 一次会发送大约 1500 个字节,因此任何比这大得多的缓冲区都只是浪费时间和空间。

标签: java memory out-of-memory heap-memory


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

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

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


    你能做些什么呢?

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

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

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

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

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-12-03
      • 2020-10-14
      • 2020-10-16
      • 2017-02-20
      • 1970-01-01
      • 1970-01-01
      • 2017-11-21
      • 1970-01-01
      相关资源
      最近更新 更多