直接缓冲存储器

2022-08-03 00:00:00 memory out-of-memory java heap-memory

我需要从Web请求返回一个相当大的文件。该文件的大小约为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获取堆转储来分析当前状态。我发现了两个相当大的字节[],这应该是我想要返回的文件。但是堆的大小只有1.6 GB左右,还没有达到配置的4 GB大小。

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


解决方案

DirectByteBuffer管理的实际内存缓冲区不在堆中分配。它们是使用UnSafe.allocateMemory分配的,它分配本机内存和。因此,增加或减少堆大小无济于事。

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


您能对此做些什么?

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

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

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

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

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

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

因此,对于这种大小的文件,更好的方法是对文件进行流式处理。要么分配一个大的(几兆字节)ByteBuffer并循环读写,或使用Files::copy(...)(javadoc)复制文件,这应该会为您负责缓冲。

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

相关文章