Java的IO性能调整

    |     2015年4月26日   |   输入输出(IO)操作   |     0 条评论   |    1208

本文多数技巧围绕磁盘文件 I/O,但同样适合网络 I/O 和窗口输出。先看底层读写,再谈压缩、格式化和串行化等高级问题;不涉及搜索算法、数据结构,也不谈系统级文件高速缓冲。

Java 对磁盘文件有两套结构:基于字节流的,以及基于字符序列的。Java 里一个字符占两个字节,不像 C 那样一个字节就是一个字符,所以从文件读字符时必须转换。这个差别在下面的计时例子里很明显。

一、低级与高级 I/O 清单

低级 I/O 相关问题:缓冲、读写文本文件、格式化的代价、随机访问。高级 I/O 问题:压缩、高速缓冲、分解、串行化、获取文件信息。

加速 I/O 的基本规则:避免访问磁盘;避免访问底层操作系统;避免方法调用;避免逐个处理字节和字符。这些规则当然不能处处都避开,否则就没有实际 I/O 了。

  • 避免访问磁盘
  • 避免访问底层操作系统
  • 避免方法调用
  • 避免个别处理字节和字符

二、用缓冲减少读写次数

对 1 MB 输入文件,三种读法的执行时间差距很大。最慢和最快大约是 17 比 1。巨大加速并不等于永远该用“自己做缓冲”的第三种:文件结束处理稍不小心就会写错,可读性也不如包装流。方法 2 对多数应用才是“正确”选择。

方法 做法 约 1MB 耗时 优点 / 缺点
1 FileInputStream.read() 每次一字节,无缓冲 6.9 秒 编码简单,适合小文件;底层调用极多,不适合大文件
2 BufferedInputStream 包装后再逐字节读 0.9 秒 避免每个字节都打到底层;占用少量内存
3 FileInputStream.read(buf) 读入自备缓冲区 0.4 秒 最快;编码稍复杂、可读性差
4 按文件长度一次读完全文件 取决于内存 底层读取最少;大文件可能耗尽内存

方法 2 和 3 都用了缓冲:大块从磁盘读入,再按字节或字符访问。缓冲是加速 I/O 的基本技术。字节用 BufferedInputStream,字符用 BufferedReader。典型 Java 缓冲区是 1024 或 2048 字节;再加大约能再快 5%~10%,比重很小。

三、方法 1:逐字节 read

FileInputStream 的 read() 每次取下一个字节,会触发大量底层运行时调用。

import java.io.*;
public class intro1 {
    public static void main(String args[]) {
        if (args.length != 1) {
            System.err.println("missing filename");
            System.exit(1);
        }
        try {
            FileInputStream fis = new FileInputStream(args[0]); // 建立指向文件的读写流
            int cnt = 0;
            int b;
            while ((b = fis.read()) != -1) { // 每次读取一个字节
                if (b == '\n')
                    cnt++;
            }
            fis.close();
            System.out.println(cnt);
        } catch (IOException e) {
            System.err.println(e);
        }
    }
}

四、方法 2:BufferedInputStream

把文件字节块读入缓冲区,再每次取一个字节。每填满一次缓冲才访问一次底层存储。

import java.io.*;
public class intro2 {
    public static void main(String args[]) {
        if (args.length != 1) {
            System.err.println("missing filename");
            System.exit(1);
        }
        try {
            FileInputStream fis = new FileInputStream(args[0]);
            BufferedInputStream bis = new BufferedInputStream(fis); // 文件流指向缓冲区
            int cnt = 0;
            int b;
            while ((b = bis.read()) != -1) { // 按块读入缓冲区,再每次取一个字节
                if (b == '\n')
                    cnt++;
            }
            bis.close();
            System.out.println(cnt);
        } catch (IOException e) {
            System.err.println(e);
        }
    }
}

五、方法 3:直接缓冲

用 fis.read(buf) 把字节块直接读进 buf,再在数组里扫换行。速度最快,但要自己处理读入长度 n。

import java.io.*;
public class intro3 {
    public static void main(String args[]) {
        if (args.length != 1) {
            System.err.println("missing filename");
            System.exit(1);
        }
        try {
            FileInputStream fis = new FileInputStream(args[0]);
            byte buf[] = new byte[2048];
            int cnt = 0;
            int n;
            while ((n = fis.read(buf)) != -1) { // 直接读入字节块到 buf
                for (int i = 0; i < n; i++) {
                    if (buf[i] == '\n')
                        cnt++;
                }
            }
            fis.close();
            System.out.println(cnt);
        } catch (IOException e) {
            System.err.println(e);
        }
    }
}

六、方法 4:缓冲整个文件

极端情况:先取文件长度,再一次读完。文件变成字节数组,很方便;问题是巨大文件可能没有足够内存。

import java.io.*;
public class readfile {
    public static void main(String args[]) {
        if (args.length != 1) {
            System.err.println("missing filename");
            System.exit(1);
        }
        try {
            int len = (int)(new File(args[0]).length());
            FileInputStream fis = new FileInputStream(args[0]);
            byte buf[] = new byte[len]; // 建立直接缓冲
            fis.read(buf);              // 读取整个文件
            fis.close();
            int cnt = 0;
            for (int i = 0; i < len; i++) {
                if (buf[i] == '\n')
                    cnt++;
            }
            System.out.println(cnt);
        } catch (IOException e) {
            System.err.println(e);
        }
    }
}

七、终端输出与参考

缓冲的另一面是向窗口终端输出文本。默认情况下,System.out(一个 PrintStream)是行缓冲的:遇到换行符后才提交输出缓冲区。

转贴来源:http://www.uml.org.cn/zjjs/201006121.asp http://blog.csdn.net/Dbzen/archive/2008/11/22/3350325.aspx

一句话总结:I/O 要快,先减少磁盘和系统调用:优先 Buffered 包装流,必要时自备缓冲区;整文件读入只适合内存装得下的情况。

转载请注明来源:Java的IO性能调整
本文链接地址:https://ai.zhousir.top/?p=580
回复 取消