当调用Files.readAllBytes时出现java.lang.OutOfMemoryError: Direct buffer memory错误

3
我有以下代码,旨在读取目录并将其压缩为tar.gz存档文件。当我将代码部署到服务器上并使用一批文件进行测试时,它可以在前几个测试批次上正常运行,但在第4或第5个批次后,即使文件批次大小保持不变且堆空间看起来很好,它仍会始终给出java.lang.OutOfMemoryError: Direct buffer memory。以下是代码:
public static void compressDirectory(String archiveDirectoryToCompress) throws IOException {
Path archiveToCompress = Files.createFile(Paths.get(archiveDirectoryToCompress + ".tar.gz"));

try (GzipCompressorOutputStream gzipCompressorOutputStream = new GzipCompressorOutputStream(
    Files.newOutputStream(archiveToCompress));
     TarArchiveOutputStream tarArchiveOutputStream = new TarArchiveOutputStream(gzipCompressorOutputStream)) {
  Path directory = Paths.get(archiveDirectoryToCompress);
  Files.walk(directory)
      .filter(path -> !Files.isDirectory(path))
      .forEach(path -> {
        String
            stringPath =
            path.toAbsolutePath().toString().replace(directory.toAbsolutePath().toString(), "")
                .replace(path.getFileName().toString(), "");
        TarArchiveEntry tarEntry = new TarArchiveEntry(stringPath + "/" + path.getFileName().toString());
        try {
          byte[] bytes = Files.readAllBytes(path); //It throws the error at this point.
          tarEntry.setSize(bytes.length);
          tarArchiveOutputStream.putArchiveEntry(tarEntry);
          tarArchiveOutputStream.write(bytes);
          tarArchiveOutputStream.closeArchiveEntry();
        } catch (Exception e) {
          LOGGER.error("There was an error while compressing the files", e);
        }
      });
}

}

这就是异常:

Caused by: java.lang.OutOfMemoryError: Direct buffer memory
    at java.nio.Bits.reserveMemory(Bits.java:658)
    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
    at sun.nio.ch.Util.getTemporaryDirectBuffer(Util.java:174)
    at sun.nio.ch.IOUtil.read(IOUtil.java:195)
    at sun.nio.ch.FileChannelImpl.read(FileChannelImpl.java:158)
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:65)
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:109)
    at sun.nio.ch.ChannelInputStream.read(ChannelInputStream.java:103)
    at java.nio.file.Files.read(Files.java:3105)
    at java.nio.file.Files.readAllBytes(Files.java:3158)
    at com.ubs.gfs.etd.reporting.otc.trsloader.service.file.GmiEodFileArchiverService.lambda$compressDirectory$4(GmiEodFileArchiverService.java:124)
    at com.ubs.gfs.etd.reporting.otc.trsloader.service.file.GmiEodFileArchiverService$$Lambda$19/183444013.accept(Unknown Source)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184)
    at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.Iterator.forEachRemaining(Iterator.java:116)
    at java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at com.ubs.gfs.etd.reporting.otc.trsloader.service.file.GmiEodFileArchiverService.compressDirectory(GmiEodFileArchiverService.java:117)
    at com.ubs.gfs.etd.reporting.otc.trsloader.service.file.GmiEodFileArchiverService.archiveFiles(GmiEodFileArchiverService.java:66)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.springframework.expression.spel.support.ReflectiveMethodExecutor.execute(ReflectiveMethodExecutor.java:113)
    at org.springframework.expression.spel.ast.MethodReference.getValueInternal(MethodReference.java:102)
    at org.springframework.expression.spel.ast.MethodReference.access$000(MethodReference.java:49)
    at org.springframework.expression.spel.ast.MethodReference$MethodValueRef.getValue(MethodReference.java:347)
    at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:88)
    at org.springframework.expression.spel.ast.SpelNodeImpl.getTypedValue(SpelNodeImpl.java:131)
    at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:330)
    at org.springframework.integration.util.AbstractExpressionEvaluator.evaluateExpression(AbstractExpressionEvaluator.java:166)
    at org.springframework.integration.util.MessagingMethodInvokerHelper.processInternal(MessagingMethodInvokerHelper.java:317)
    ... 93 more

我认为有一个缓冲内存泄漏问题,因为它在前4个测试批次上无缺陷地工作,但在此后一直出现java.lang.OutOfMemoryError: Direct buffer memory错误。但我不知道如何解决它。我在这里看到了一个潜在的解决方案,使用Cleaner方法:http://www.java67.com/2014/01/how-to-fix-javalangoufofmemoryerror-direct-byte-buffer-java.html,但我不知道它是否适用于这种情况。
------------------------编辑------------------------
我发现使用IOUtils和缓冲输入流来压缩文件的另一种方法可以解决问题,更新代码:
  public static void compressDirectory(String archiveDirectoryToCompress) throws IOException {
Path archiveToCompress = Files.createFile(Paths.get(archiveDirectoryToCompress + ".tar.gz"));

try (GzipCompressorOutputStream gzipCompressorOutputStream = new GzipCompressorOutputStream(
    Files.newOutputStream(archiveToCompress));
     TarArchiveOutputStream tarArchiveOutputStream = new TarArchiveOutputStream(gzipCompressorOutputStream)) {
  Path directory = Paths.get(archiveDirectoryToCompress);
  Files.walk(directory)
      .filter(path -> !Files.isDirectory(path))
      .forEach(path -> {
        TarArchiveEntry tarEntry = new TarArchiveEntry(path.toFile(),path.getFileName().toString());
        try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(path.toString()))) {
          tarArchiveOutputStream.putArchiveEntry(tarEntry);
          IOUtils.copy(bufferedInputStream, tarArchiveOutputStream);
          tarArchiveOutputStream.closeArchiveEntry();
        } catch (Exception e) {
          LOGGER.error("There was an error while compressing the files", e);
        }
      });
}

}


你为你的应用程序分配了多少堆大小?另外,将文件中的所有字节读入内存是一个不好的决定,最好使用InputStream。 - eg04lt3r
我分配了512MB的堆空间。当我监视jconsole时,我发现当压缩代码被调用时,堆内存使用似乎并没有太大变化,因为我同时监视日志文件和jconsole。 - ISHIRO
请使用我的方法,因为IOUtils.copy在复制完成后不会关闭流。在这种情况下,当尝试压缩大量文件(~1000个文件)时,您可能会遇到另一个异常。或者您可以在finally块中关闭它们。根据IOUtils.copy文档,您的示例中包装BufferedInputStream是不必要的,它具有内部缓冲。 - eg04lt3r
3个回答

3
当将文件加载到内存时,Java会使用一个名为直接内存池的不同于堆池的池来分配一系列DirectByteBuffers。这些缓冲区也附带有Deallocator类,该类负责在文件不再需要时释放该内存。默认情况下,这些Deallocators在垃圾回收期间运行。
我怀疑正在发生的事情(这是我以前看到过的)是您的程序并没有很好地使用堆,在垃圾回收不经常运行以释放这些DirectByteBuffers。因此,您可以尝试以下两种方法之一:要么使用-XX:MaxDirectMemorySize增加直接内存池的大小,要么通过调用System.gc()定期强制进行垃圾回收。

1
实际上,您只需调用file.length()即可获取文件大小。尝试更改从文件读取字节的方式:
tarArchiveOutputStream.write(IOUtils.toByteArray(new FileInputStream(path.toFile())));

来自 Apache Commons IO 包的 IOUtils 类 (http://commons.apache.org/proper/commons-io/)。我认为它应该有助于解决你的问题。在某些情况下,@afretas 的建议是有用的。


感谢您的帮助,我决定使用BufferedInputStream和IOUtils.copy,并已经编辑了我的帖子,附上了新代码。谢谢。 - ISHIRO
我很高兴能够帮到你! - eg04lt3r

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接