Java FileOutputStream连续关闭需要很长时间

时间:2014-08-14 12:59:21

标签: java java-io

我面临着一种奇怪的情况。

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

但是接下来,当我再次运行时,每次连续关闭大约需要1500-2000毫秒! 删除此文件时,持续时间将减少到1毫秒。

我缺少一些必要的java.io知识吗?

它似乎与操作系统有关。我在ArchLinux上运行(在Windows 7上运行的相同代码一直在20ms以下)。请注意,它是否在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

3 个答案:

答案 0 :(得分:2)

@Duncan提出了以下解释:

  

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

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

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

执行第二次复制时,操作系统会尝试获取用于读取和写入的缓冲区缓存页面。但由于缓冲区缓存中充满了脏页,因此会反复阻止读写调用,等待空闲页面可用。但是在可以回收脏页之前,需要将页面中的数据写入光盘。最终结果是复制速度降低到有效数据写入速率。


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

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

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

答案 1 :(得分:1)

你好像有点兴趣。在Linux下,允许某人持有原始文件的文件句柄,当你打开它时,实际上删除目录条目并重新开始。这不会打扰原始文件(句柄)。在结束时,可能会发生一些磁盘目录工作。

使用IOUtils.copyLarge和Files.copy测试它:

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

(我曾经看到一个名为copyLarge的IOUtils.copy,但Files.copy应该表现得很好。)

答案 2 :(得分:0)

请注意,这个问题被问到了,因为我很好奇为什么会这样,这并不意味着要测量复制吞吐量。

总结:

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

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

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