java 直接缓冲存储器

cig3rfwq  于 2022-12-21  发布在  Java
关注(0)|答案(2)|浏览(127)

我需要从一个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进行了一次堆转储来分析当前状态。我发现了两个相当大的byte[],这应该是我想要返回的文件。但堆的大小只有1.6gb左右,还没有接近配置的4gb。
根据另一个答案(https://stackoverflow.com/a/39984276/5126654)在一个类似的问题中,我尝试在返回这个文件之前运行手动gc。问题仍然发生,但现在只有spardic。问题发生在一段时间后,但当我厌倦了再次运行相同的请求时,似乎垃圾收集处理了导致问题的任何东西,但这是不够的,因为问题显然仍然可能发生。有没有其他方法来避免这个内存问题?

jjjwad0x

jjjwad0x1#

DirectByteBuffer管理的实际内存缓冲区不在堆中分配,而是使用分配"本机内存"的Unsafe. allocateMemory分配的,因此增加或减少堆大小没有帮助。
当GC检测到DirectByteBuffer不再被引用时,就会使用Cleaner来释放本机内存。然而,这发生在收集后阶段,因此如果直接缓冲区的需求/周转量太大,收集器可能无法跟上。如果发生这种情况,就会得到OOME。
你能做些什么呢?
AFAIK,你唯一能做的就是强制更频繁的垃圾收集。但是这会影响性能。我不认为这是一个有保证的解决方案。
真正的解决办法是采取不同的方法。
您看到您正在从Web服务器提供大量非常大的文件,stacktrace显示您正在使用Files::readAllBytes将它们加载到内存中,然后(大概)使用单个write发送它们。大概您这样做是为了获得尽可能快的下载时间。这是一个错误:

  • 传输文件的瓶颈 * 可能 * 不是从磁盘读取数据的过程,真正的瓶颈 * 通常 * 是通过网络上的TCP流发送数据,或将其写入客户端的文件系统。
  • 如果你要连续读取一个大文件,现代的Linux操作系统通常会提前读取大量的磁盘块,并将这些块保存在(OS)缓冲区缓存中,这将减少应用程序进行read系统调用时的延迟。

因此,对于这种大小的文件,更好的办法是流式传输文件,要么分配一个大的(几兆字节)ByteBuffer并在循环中读/写,* 要么 * 使用Files::copy(...)(javadoc)复制文件,这样就可以为您提供缓冲。
(也可以选择使用Map到Linux sendfile syscall的内容,这会将数据从一个文件描述符复制到另一个文件描述符,而不会将其写入用户空间缓冲区。)

4ktjp1zp

4ktjp1zp2#

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

相关问题