调整JavaTM 的I/O性能

这篇文章讨论并举例阐述了提高JavaTM I/O性能的多种技术。绝大多数技术是围绕着磁盘文件I/O的调整来谈 的,但是,有些技术对网络I/O和视窗输出也同样适用。首先介绍的技术包含底层I/O问题,然后对诸如压 缩、格式化和序列化这样的高层I/O进行讨论。但是,请注意,本讨论不涉及应用设计问题, 搜索算法和数 据结构的选择,也不讨论类似文件高速缓存(file caching)这样的系统级问题。

当讨论Java I/O时,Java编程语言所假定的两种不同的磁盘文件组织是没有任何意义的。这两种磁盘文件组 织,一种基于字节流,另一种基于字符序列。在 Java语言中,一个字符使用两个字节表示,而不是象C语言 那样使用一个字节表示一个字符。正因为如此,从文件中读取字符时需要一些转换。在某些情况下,这样的 区别非常重要,我们将用几个例子对此进行说明。

底层I/O问题
  • 简介
  • 加速I/O的基本规则
  • 缓冲
  • 读/写文本文件
  • 格式化的开销
  • 随机存储

高层I/O问题
  • 压缩
  • 高速缓存
  • 标志化(Tokenization)
  • 序列化(Serialization)
  • 获取文件信息
  • 更多的信息
加速I/O的基本规则

作为开始讨论的一种方法,下面列出了加速I/O的一些基本规则:

1.避免访问磁盘
2.避免访问下面的操作系统
3.避免方法调用
4.避免对字节和字符的单独处理

显然,这些规则不能被全面而严格地应用,因为如果那样的话,I/O就不可能工作了。但是,为了查看规则是 如何被应用的,就考虑下面的三个例子,这些例子计算一个文件中换行符('\n')的数目。

方法一:读取的方法

第一个方法简单地利用一个文件输入流(FileInputStream)上的读方法:


      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); 

              } 

          } 

      }


然而,这个方法触发了大量对底层运行系统的调用,这就是FileInputStream.read, 返回文件下一个字节的本 机方法。

方法二:采用一个大缓冲区

第二种方法通过采用一个大缓冲区,避免了上述问题:


      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); 

              } 

          } 

      }

BufferedInputStream.read从输入缓冲区中获取下一个字节,极少访问底层系统。

方法三:直接缓冲(Direct Buffering)

第三种方法避免使用缓冲的输入流(BufferedInputStream),而直接进行缓冲,因此避免了读取方法的调用:


      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) { 

                      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); 

              } 

          } 

      }



对于1MB的输入文件,以秒为单位,各个程序的执行时间为: 



      intro1    6.9 

      intro2    0.9 

      intro3    0.4


或者,在最快和最慢之间存在一个17比1的差距。

这巨大的加速性能并没有必然地证明,应该总是效仿第三种方法,因为此方法中需要自己进行缓冲。如果事 先没有进行仔细的实现,这样的方法可能容易造成错误,特别是在处理文件结束 (end-of-file)事件时。它 也可能在可读性上比其他的方法差。但是,记住时间都花费到什么地方去了,记住在需要时如何纠正是很有 用的。

对绝大多数应用程序而言,方法二可能是正确的选择。

缓冲

方法二和方法三使用了缓冲技术,其中,文件中的一整块从磁盘中读取出来,然后再一 次一个字节或者字符 地进行访问。缓冲是加速I/O的一种基本和重要的技术,而且许多Java类都支持缓冲(BufferedInputStream用于 字节,BufferedReader用于字符)。

一个明显的问题是:是否缓冲区越大就能够使I/O越快呢?Java缓冲区典型的缺省值是1024或者2048个字节。 大于此值的缓冲区可能能够帮助加速I/O,但通常只有几个百分点,即5%到10%。

方法四:整个文件

这个极端的例子需要确定文件的长度,然后将整个文件读取到缓冲区中。


      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)是行缓 冲的,也就是说,当遇到一个换行符时输出队列被清空。对于交互式应用来说,这是很重要的,阅 <淘宝热门商品:

小小豆叮

0 Responses to "调整JavaTM 的I/O性能"

发表评论