使用ZipFileSystem压缩一个巨大的文件夹会导致OutOfMemoryError错误。

17

java.nio包以一种优美的方式处理zip文件,将它们视为文件系统。这使我们能够像对待普通文件一样处理zip文件内容。因此,只需使用Files.copy将所有文件复制到zip文件中,就可以轻松地压缩整个文件夹。由于子文件夹也需要被复制,所以我们需要一个访问者:

 private static class CopyFileVisitor extends SimpleFileVisitor<Path> {
    private final Path targetPath;
    private Path sourcePath = null;
    public CopyFileVisitor(Path targetPath) {
        this.targetPath = targetPath;
    }

    @Override
    public FileVisitResult preVisitDirectory(final Path dir,
    final BasicFileAttributes attrs) throws IOException {
        if (sourcePath == null) {
            sourcePath = dir;
        } else {
        Files.createDirectories(targetPath.resolve(sourcePath
                    .relativize(dir).toString()));
        }
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(final Path file,
    final BasicFileAttributes attrs) throws IOException {
    Files.copy(file,
        targetPath.resolve(sourcePath.relativize(file).toString()), StandardCopyOption.REPLACE_EXISTING);
    return FileVisitResult.CONTINUE;
    }
}

这是一个简单的“递归复制目录”访问器。它用于递归地复制目录。然而,使用ZipFileSystem,我们也可以将其用于将目录复制到zip文件中,像这样:

public static void zipFolder(Path zipFile, Path sourceDir) throws ZipException, IOException
{
    // Initialize the Zip Filesystem and get its root
    Map<String, String> env = new HashMap<>();
    env.put("create", "true");
    URI uri = URI.create("jar:" + zipFile.toUri());       
    FileSystem fileSystem = FileSystems.newFileSystem(uri, env);
    Iterable<Path> roots = fileSystem.getRootDirectories();
    Path root = roots.iterator().next();

    // Simply copy the directory into the root of the zip file system
    Files.walkFileTree(sourceDir, new CopyFileVisitor(root));
}

这是我称之为优雅的整个文件夹压缩方式。然而,当在一个巨大的文件夹(约 3 GB)上使用此方法时,会收到OutOfMemoryError(堆空间)错误。当使用常规的 zip 处理库时,不会出现此错误。因此,看起来 ZipFileSystem 处理复制的方式非常低效:太多要写入的文件被保存在内存中,导致OutOfMemoryError 错误。

为什么会出现这种情况?使用ZipFileSystem一般被认为是低效的(在内存消耗方面),还是我在这里做错了什么?

2个回答

33

我查看了ZipFileSystem.java文件并且认为我找到了内存消耗的源头。默认情况下,实现是使用ByteArrayOutputStream作为压缩文件的缓冲区,这意味着它受到JVM分配的内存量的限制。

有一个(未经记录的)环境变量,我们可以使用它来使实现使用临时文件("useTempFile")。它的工作方式如下:

Map<String, Object> env = new HashMap<>();
env.put("create", "true");
env.put("useTempFile", Boolean.TRUE);

更多细节请参见:http://www.docjar.com/html/api/com/sun/nio/zipfs/ZipFileSystem.java.html,有趣的行是96、1358和1362。


4
非常感谢您对此事的调查。在使用http://goo.gl/woa0Ab并同时压缩文件时,当`useTempFile=TRUE`时观察临时目录,似乎每个文件都会独立并行地压缩到一个单独的临时压缩文件中,然后将所有这些文件连接成一个文件。然后将该文件原子地重命名为归档名称。可惜这没有记录,更遗憾的是Java标准库仍然没有流式并行压缩。 - Tomáš Dvořák
1
谢谢回答 :),但是临时文件之后会被删除吗? - VitalyT
@VitalyT,是的,它们已经被删除了,请在source中搜索tmppaths - Marcono1234
2
最糟糕的是,useTempFile 必须是一个带有 true 值的布尔类型,而 create 必须是一个值为 true 的字符串。难以置信这甚至成为了标准。 - Philipp

-3

你必须准备好jvm,以允许使用-Xms {memory} -Xmx {memory}这些内存量。

我建议您检查计算磁盘空间的目录并设置限制,在1GB以下使用内存文件系统,在1GB以上使用磁盘文件系统。

另外,请检查方法的并发性,您不希望有超过1个线程压缩3GB的文件。


2
抱歉,但这个答案完全没有帮助。1)我知道如何增加堆大小,这不是问题。2)什么是“内存文件系统”和“磁盘文件系统”?3)从代码中可以看出,该方法不是并发的。 - gexicide
@gexicide 请检查我的回复,如果它解决了你的问题(就像对其他人一样),请将其标记为正确答案。谢谢。 - Diego Giagio

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