陷阱 - 在无缓冲的流上写入小写是低效的

请考虑以下代码将一个文件复制到另一个文件:

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new FileInputStream(args[0]);
             OutputStream os = new FileOutputStream(args[1])) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

(我们已经考虑省略了正常的参数检查,错误报告等等,因为它们与此示例的要点无关。)

如果你编译上面的代码并使用它来复制一个巨大的文件,你会发现它非常慢。实际上,它至少比标准 OS 文件复制实用程序慢几个数量级。

在这里添加实际的性能测量!

上面的示例很慢(在大文件的情况下)的主要原因是它在无缓冲的字节流上执行单字节读取和单字节写入。提高性能的简单方法是使用缓冲流包装流。例如:

import java.io.*;

public class FileCopy {

    public static void main(String[] args) throws Exception {
        try (InputStream is = new BufferedInputStream(
                     new FileInputStream(args[0]));
             OutputStream os = new BufferedOutputStream(
                     new FileOutputStream(args[1]))) {
           int octet;
           while ((octet = is.read()) != -1) {
               os.write(octet);
           }
        }
    }
}

这些微小的变化将使数据复制速率提高至少几个数量级,具体取决于各种平台相关因素。缓冲流包装器使数据以更大的块读取和写入。实例都将缓冲区实现为字节数组。

  • 使用 is,数据一次从文件读入缓冲区几千字节。当调用 read() 时,实现通常会从缓冲区返回一个字节。如果缓冲区已清空,它将仅从基础输入流中读取。

  • os 的行为是类似的。调用 os.write(int) 将单个字节写入缓冲区。仅当缓冲区已满或者刷新或关闭 os 时,才会将数据写入输出流。

基于字符的流怎么样?

你应该知道,Java I / O 提供了不同的 API 来读取和写入二进制和文本数据。

  • InputStreamOutputStream 是基于流的二进制 I / O 的基本 API
  • ReaderWriter 是基于流的文本 I / O 的基本 API。

对于文本 I / O,BufferedReaderBufferedWriterBufferedInputStreamBufferedOutputStream 的等价物。

为什么缓冲流会产生这么大的差异?

缓冲流有助于提高性能的真正原因是应用程序与操作系统对话的方式:

  • Java 应用程序中的 Java 方法或 JVM 的本机运行时库中的本机过程调用很快。它们通常采用几条机器指令,对性能影响最小。

  • 相比之下,对操作系统的 JVM 运行时调用并不快。它们涉及一种称为系统调用的东西。系统调用的典型模式如下:

    1. 将 syscall 参数放入寄存器。
    2. 执行 SYSENTER 陷阱指令。
    3. 陷阱处理程序切换到特权状态并更改虚拟内存映射。然后它调度到代码来处理特定的系统调用。
    4. 系统调用处理程序检查参数,注意它没有被告知访问用户进程不应该看到的内存。
    5. 执行系统调用特定的工作。在 read 系统调用的情况下,这可能涉及:
      1. 检查在文件描述符的当前位置是否有要读取的数据
      2. 调用文件系统处理程序从磁盘(或存储的任何位置)获取所需数据到缓冲区缓存中,
      3. 将数据从缓冲区高速缓存复制到 JVM 提供的地址
      4. 调整 thstream 指针文件描述符位置
    6. 从系统调用返回。这需要再次更改 VM 映射并切换到特权状态。

可以想象,执行单个系统调用可以获得数千条机器指令。保守地说,比常规方法调用至少长两个数量级。 (可能是三个或更多。)

鉴于此,缓冲流产生重大影响的原因是它们大大减少了系统调用的数量。缓冲输入流不是为每个 read() 调用执行系统调用,而是根据需要将大量数据读入缓冲区。大多数对缓冲流的 read() 调用都会进行一些简单的边界检查并返回之前读过的 byte。类似的推理适用于输出流情况,也适用于字符流情况。

(有些人认为缓冲的 I / O 性能来自于读取请求大小与磁盘块大小,磁盘旋转延迟等等之间的不匹配。实际上,现代操作系统使用了许多策略来确保应用程序通常不需要等待磁盘。这不是真正的解释。)

缓冲流总是赢吗?

不总是。如果你的应用程序要进行大量读取或写入,缓冲流肯定是一个胜利。但是,如果你的应用程序只需要对大型的 byte[]char[] 执行大量读取或写入操作,那么缓冲流将不会给你带来任何实际好处。实际上甚至可能存在(微小的)性能损失。

这是用 Java 复制文件的最快方法吗?

不,不是。当你使用 Java 的基于流的 API 来复制文件时,会产生至少一个额外的内存到内存的数据副本的成本。如果你使用 NIO ByteBufferChannel API,可以避免这种情况。 ( 在此处添加指向单独示例的链接。