【问题标题】:Java FileOutputStream consecutive close takes a long timeJava FileOutputStream 连续关闭需要很长时间
【发布时间】:2014-10-08 03:22:03
【问题描述】:

我正面临一个有点奇怪的情况。

我正在将一个大小约为 500MB 的文件从 FileInputStream 复制到 FileOutputStream。 它进展顺利(大约需要 500 毫秒)。当我在 FIRST 时间关闭此 FileOutputStream 时,大约需要 1 毫秒。

但问题来了,当我再次运行此程序时,每次连续关闭大约需要 1500-2000 毫秒! 当我删除这个文件时,持续时间下降到 1 毫秒。

我缺少一些基本的java.io 知识吗?

这似乎与操作系统有关。我在 ArchLinux 上运行(在 Windows 7 上运行的相同代码一直都在 20 毫秒以下)。请注意,它是否在 OpenJDK 或 Oracle 的 JDK 中运行并不重要。硬盘是带有 ext4 文件系统的固态硬盘。

这是我的测试代码:

public void copyMultipleTimes() throws IOException {
    copy();
    copy();
    copy();
    new File("/home/d1x/temp/500mb.out").delete();
    copy();
    copy();
    // Runtime.getRuntime().exec("sync") => same results
    // Thread.sleep(30000) => same results
    // combination of sync & sleep => same results
    copy();
}

private void copy() throws IOException {
    FileInputStream fis = new FileInputStream("/home/d1x/temp/500mb.in");
    FileOutputStream fos = new FileOutputStream("/home/d1x/temp/500mb.out");
    IOUtils.copy(fis, fos); // copyLarge => same results
    // copying takes always the same amount of time, only close "enlarges"

    fis.close(); // input stream close this is always fast
    // fos.flush(); // has no effect 
    // fos.getFD().sync(); // Solves the problem but takes ~2.5s

    long start = System.currentTimeMillis();
    fos.close();
    System.out.println("OutputStream close took " + (System.currentTimeMillis() - start) + "ms");
}

那么输出是:

OutputStream close took 0ms
OutputStream close took 1951ms
OutputStream close took 1934ms
OutputStream close took 1ms
OutputStream close took 1592ms
OutputStream close took 1727ms

【问题讨论】:

  • 在 Ubuntu 13.10 上运行的 Java7 具有相同的行为。
  • 你能尝试在close之前显式地flushfos(当然在long start = System.currentTimeMillis();之前)吗?
  • 这确实似乎是特定于操作系统的,因为我无法在 Win7 下重现它(得到不一致的结果)
  • 这是真的克里斯,但“经典”硬盘上的时间是一样的。
  • 如果它与 Java 有任何关系,我会感到惊讶。用'cp'试试。我希望你会得到非常相似的结果。这很可能与固态硬盘的工作方式有关。

标签: java java-io


【解决方案1】:

@Duncan 提出以下解释:

第一次调用 close() 很快返回,但操作系统仍在将数据刷新到磁盘。在上一次刷新完成之前,对 close() 的后续调用无法完成。

我认为这接近标记,但并不完全正确。

我认为这里实际发生的情况是,第一个副本正在用大量脏页填满操作系统的文件缓冲区缓存。将脏页刷新到磁盘的内部守护程序可能会开始处理它们,但当您启动第二个副本时它仍然会运行。

当您进行第二次复制时,操作系统会尝试获取缓冲区缓存页面以进行读取和写入。但是由于缓冲区缓存中充满了脏页,因此读取和写入调用被反复阻塞,等待空闲页变为可用。但是在回收脏页之前,需要将页中的数据写入磁盘。最终结果是副本减慢到有效数据写入速率。


30 秒的暂停可能不足以完成将脏页刷新到磁盘。

您可以尝试的一件事是在副本之间使用fsync(fd)fdatasync(fd)。在 Java 中,这样做的方法是调用FileDescriptor.sync()

现在,我不能说这是否会提高总复制吞吐量,但我希望 sync 操作在写出(仅)一个文件方面比依赖页面驱逐算法做得更好它。

【讨论】:

  • 为什么要这么做?
  • 我尝试在 fos.close() 之前添加 fos.getFD().sync()。它确实有助于关闭现在只需要 1 毫秒(但同步需要 2-2.5 秒)。
  • 您是否认为添加此同步(即使它比流复制需要更多时间)证明了您的想法?如果是,我可能会将您的答案标记为正确 - 这是迄今为止最好的解释。
  • 不...我认为这不能证明这一点。不过它支持我的理论。此外,同步可能不是最大化复制吞吐量的方法。但是话又说回来,我不相信您的测试是衡量复制吞吐量的现实方法。 (反复复制同一个文件....)
  • 将多个输入复制到一个输出文件(除非您要追加)同样不现实。我认为这些只是 Linux 开发人员认为不值得调整的“用例”。
【解决方案2】:

你似乎在做一些有趣的事情。在 Linux 下,允许某人持有原始文件的文件句柄,当您打开它时,实际上删除了目录条目并重新开始。这不会打扰原始文件(句柄)。关闭时,可能会发生一些磁盘目录工作。

使用 IOUtils.copyLarge 和 Files.copy 对其进行测试:

Path target = Paths.get("/home/d1x/temp/500mb.out");
Files.copy(fis, target, StandardCopyOption.REPLACE_EXISTING);

(我曾经看到一个 IOUtils.copy,它只是调用了 copyLarge,但 Files.copy 应该很好用。)

【讨论】:

  • copyLarge 没有效果,但使用 Files.copy 工作正常 - 它总是只需要复制所需的时间(没有流关闭,并且 Files.copy 本身与 IOUtils 花费相同的时间)。
  • 因此,将关闭文件留给 Files.copy,这样做是正确的。 (快乐)也许 IOUtils 在 InputStream 周围包装了一个 BufferedInputStream 并且在刷新/关闭时发生了一些奇怪的事情?不明白,因为 IOUtils 现在已经过时了……
  • 它可能没有连接到 IOUtils 而是 FileOutputStream 处理。如果您自己进行复制(使用自己的 byte[] 缓冲区和 outputStream.write-s),结果将是相同的(关闭之间的长时间延迟)。如果您查看 IOUtils.copy 内部,您会发现它没有任何魔力(while 循环中的简单 outputStream.write)
  • 那么值得研究一下Files.copy(InputStream, Path) - 有时。
  • 如果文件存在,复制前有删除。这就是为什么它可以正常工作的原因(示例也可以在您在复制之前调用 delete 时)。
【解决方案3】:

请注意,之所以问这个问题是因为我很好奇为什么会发生这种情况,它并不是要衡量复制吞吐量。

总结一下:

正如EJP 所说,整个事情没有连接到Java。如果在 bash 脚本中运行多个连续的 cp 命令,结果是相同的。

为什么会发生这种情况的最佳答案是 Stephen 的答案 - 复制调用之间的 fsync 消除了问题(但 fsync 本身需要大约 2.5 秒)。

解决此问题的最佳方法是使用Files.copy(I, o, REPLACE_EXISTING)(如Joop 的答案)=> 首先检查目标文件是否存在,如果存在则删除它(而不是“覆盖”)。然后你可以快速写和关闭流。

【讨论】:

    猜你喜欢
    • 2022-01-19
    • 1970-01-01
    • 2014-03-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-29
    • 1970-01-01
    相关资源
    最近更新 更多