Java NIO的FileChannel与FileOutputStream性能/用途对比

177

我正在尝试弄清楚使用NIO的FileChannel和普通的FileInputStream/FileOutputStream读写文件到文件系统时是否存在性能差异(或优势)。我观察到,在我的机器上,两者的表现水平相同,而且很多时候使用FileChannel的方式会更慢。请问这两种方法的比较细节。这是我使用的代码,我测试的文件大小约为350MB。如果我不考虑随机访问或其他高级功能,使用基于NIO的类进行文件I/O是一个好选择吗?

package trialjavaprograms;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class JavaNIOTest {
    public static void main(String[] args) throws Exception {
        useNormalIO();
        useFileChannel();
    }

    private static void useNormalIO() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        InputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        byte[] buf = new byte[64 * 1024];
        int len = 0;
        while((len = is.read(buf)) != -1) {
            fos.write(buf, 0, len);
        }
        fos.flush();
        fos.close();
        is.close();
        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }

    private static void useFileChannel() throws Exception {
        File file = new File("/home/developer/test.iso");
        File oFile = new File("/home/developer/test2");

        long time1 = System.currentTimeMillis();
        FileInputStream is = new FileInputStream(file);
        FileOutputStream fos = new FileOutputStream(oFile);
        FileChannel f = is.getChannel();
        FileChannel f2 = fos.getChannel();

        ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024);
        long len = 0;
        while((len = f.read(buf)) != -1) {
            buf.flip();
            f2.write(buf);
            buf.clear();
        }

        f2.close();
        f.close();

        long time2 = System.currentTimeMillis();
        System.out.println("Time taken: "+(time2-time1)+" ms");
    }
}

5
transferTo/transferFrom 更符合在复制文件时的传输惯例。无论哪种方法使用,都不应该让您的硬盘变得更快或更慢,尽管我猜测如果以小块读取并导致读写头花费过多时间寻道,可能会出现问题。 - Tom Hawtin - tackline
1
您没有提到您使用的操作系统,或者使用的JRE供应商和版本。 - Tom Hawtin - tackline
抱歉,我使用的是FC10和Sun JDK6。 - Keshav
7个回答

212

我对更大的文件大小的经验是,java.niojava.io 更快。坚定地更快。速度可以达到超过250%。不过,我已经消除了明显的瓶颈,我建议您的微基准测试可能会受到影响。对于可能需要调查的区域:

缓冲区大小。 基本上你所拥有的算法是

  • 从磁盘复制到缓冲区
  • 从缓冲区复制到磁盘

我的经验是,这个缓冲区大小非常适合进行调整。我已经为应用程序的某一部分选择了4KB,为另一部分选择了256KB。我怀疑您的代码在使用如此大的缓冲区时会遇到问题。请使用1KB、2KB、4KB、8KB、16KB、32KB和64KB大小的缓冲区运行一些基准测试,以证明这一点。

不要执行读写同一磁盘的java基准测试。

如果这样做,那么您真正进行的是磁盘而不是Java的基准测试。我还建议,如果您的CPU没有忙碌,那么您可能正在经历其他瓶颈。

如果不需要,请勿使用缓冲区。

如果您的目标是另一张磁盘或网卡,为什么要复制到内存中?对于更大的文件,产生的延迟是非常显著的。像其他人所说,使用 FileChannel.transferTo()FileChannel.transferFrom()。这里的关键优势在于,如果存在,JVM使用操作系统对DMA(直接内存访问)的访问。 (这取决于具体实现,但通用CPU上的现代Sun和IBM版本都可以正常运行。)数据将直接从磁盘传输到总线,然后传输到目标…绕过任何通过RAM或CPU的电路。

我日夜投入的网络应用程序对IO操作要求非常高。我进行了微基准测试和实际场景测试,并将结果发布在我的博客上,请看:

使用生产环境和数据

微基准测试很容易被扭曲。如果可以的话,请努力收集来自于你计划执行的确切任务,在预期负载下,使用预期硬件的数据。

我的基准测试非常可靠,因为它们是在生产系统、强大的系统、负载下、从日志中收集的。而不是在我看着JVM操作我的笔记本电脑的7200 RPM 2.5英寸SATA硬盘时进行的测试。

你运行在什么上面?这很重要。


@Stu Thompson - 感谢您的帖子。我因为正在研究同一主题而发现了您的答案。我正在尝试理解nio向Java程序员公开的操作系统改进,其中包括DMA和内存映射文件等几个方面。您是否遇到过更多类似的改进?P.S-您的博客链接已经失效。 - Andy Dufresne
@AndyDufresne 我的博客目前无法访问,将在本周稍后恢复——正在迁移中。 - Stu Thompson
15
以下是档案馆网站上的博客链接:http://web.archive.org/web/20120815094827/http://geekomatic.ch/2008/09.htmlhttp://web.archive.org/web/20120821114802/http://geekomatic.ch/2009/01.html - Arthur Edelstein
1
只是将文件从一个目录复制到另一个目录怎么样?(每个不同的磁盘驱动器) - Deckard
1
有趣的内容:http://mailinator.blogspot.in/2008/02/kill-myth-please-nio-is-not-faster-than.html - Jeryl Cook
1
NIO对于较大的文件速度更快。我可以问一下这里的“较大”是指什么大小吗?较大的文件是>1MB,>100MB,>350MB还是更多,就像最初的问题中提到的那样? - Smithfield

40
如果你想比较的是文件复制的性能,那么对于通道测试,你应该这样做:
final FileInputStream inputStream = new FileInputStream(src);
final FileOutputStream outputStream = new FileOutputStream(dest);
final FileChannel inChannel = inputStream.getChannel();
final FileChannel outChannel = outputStream.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
outChannel.close();
inputStream.close();
outputStream.close();

这种方法不会比从一个通道缓冲到另一个通道慢,并且可能会更快。根据Javadocs的说法:

许多操作系统可以将字节直接从文件系统缓存传输到目标通道,而无需实际复制它们。


9

回答问题的“有用”部分:

使用FileChannel而不是FileOutputStream的一个相当微妙的陷阱是,如果从处于中断状态的线程执行其任何阻塞操作(例如read()write()),则会导致通道突然关闭,并出现 java.nio.channels.ClosedByInterruptException

现在,如果FileChannel用于线程的主要功能之一,并且设计已经考虑到了这一点,那么这可能是一件好事。

但是,如果它被某些辅助功能(如日志记录功能)使用,那么这也可能是令人讨厌的。例如,如果由一个被中断的线程调用日志记录函数,则可能会发现日志输出突然关闭。

不幸的是,这种情况非常微妙,因为不考虑这一点可能会导致影响写入完整性的错误。[1][2]


8

根据我的测试(Win7 64位,6GB RAM,Java6),NIO的transferFrom仅在小文件上表现快速,而在大文件上变得非常缓慢。NIO的databuffer翻转始终优于标准IO。

  • 复制1000个2MB的文件

    1. NIO(transferFrom)约为2300ms
    2. NIO(直接datababuffer 5000b翻转)约为3500ms
    3. 标准IO(buffer 5000b)约为6000ms
  • 复制100个20mb的文件

    1. NIO(直接datababuffer 5000b翻转)约为4000ms
    2. NIO(transferFrom)约为5000ms
    3. 标准IO(buffer 5000b)约为6500ms
  • 复制1个1000mb的文件

    1. NIO(直接datababuffer 5000b翻转)约为4500s
    2. 标准IO(buffer 5000b)约为7000ms
    3. NIO(transferFrom)约为8000ms

transferTo()方法适用于文件块;并不是旨在用作高级文件复制方法: 如何在Windows XP中复制大文件?


3

如果您没有使用transferTo功能或非阻塞特性,则传统IO和NIO(2)之间没有区别,因为传统IO映射到NIO。

但是,如果您可以使用NIO功能,如transferFrom/To或想要使用缓冲区,则当然应该选择NIO。


3

我测试了使用FileInputStream和FileChannel解码base64编码文件的性能。在我的实验中,我测试了相当大的文件,传统IO始终比NIO快一点。

在以前的JVM版本中,由于几个IO相关类中的同步开销,FileChannel可能具有优势,但现代JVM非常擅长消除不需要的锁定。


-1

我的经验是,对于小文件,NIO 更快。但是当涉及到大文件时,FileInputStream/FileOutputStream 更快。


6
你是不是搞混了?我的经验是相对于java.iojava.nio在处理较大的文件时更快,而不是更小的文件。 - Stu Thompson
不,我的经验正好相反。只要文件足够小可以映射到内存中,java.nio就很快。如果文件变得更大(200 MB或更多),那么java.io会更快。 - tangens
哇,完全和我不一样。请注意,您不必将文件映射到才能读取它-可以使用FileChannel.read()进行读取。使用java.nio读取文件并不只有一种方法。 - Stu Thompson
2
@tangens 你有检查过这个吗? - sinedsem

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