林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka
摘要:本文主要讲了java中内存映射的原理及过程,与传统IO进行了对比,最后,用实例说明了结果。
一、java中的内存映射IO和内存映射文件是什么?
内存映射文件非常特别,它允许Java程序直接从内存中读取文件内容,通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得IO操作非常快。加载内存映射文件所使用的内存在Java堆区之外。Java编程语言支持内存映射文件,通过java.nio包和MappedByteBuffer 可以从内存直接读写文件。
内存映射文件
内存映射文件,是由一个文件到一块内存的映射。Win32提供了允许应用程序把文件映射到一个进程的函数 (CreateFileMapping)。内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。
内存映射IO
在传统的文件IO操作中,我们都是调用操作系统提供的底层标准IO系统调用函数 read()、write() ,此时调用此函数的进程(在JAVA中即java进程)由当前的用户态切换到内核态,然后OS的内核代码负责将相应的文件数据读取到内核的IO缓冲区,然 后再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO操作。这么做是为了减少磁盘的IO操作,为了提高性能而考虑的,因为我们的程序访问一般都带有局部性,也就是所 谓的局部性原理,在这里主要是指的空间局部性,即我们访问了文件的某一段数据,那么接下去很可能还会访问接下去的一段数据,由于磁盘IO操作的速度比直接 访问内存慢了好几个数量级,所以OS根据局部性原理会在一次 read()系统调用过程中预读更多的文件数据缓存在内核IO缓冲区中,当继续访问的文件数据在缓冲区中时便直接拷贝数据到进程私有空间,避免了再次的低 效率磁盘IO操作。其过程如下
内存映射文件和之前说的 标准IO操作最大的不同之处就在于它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一 部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。
内存映射的优缺点
内存映射IO最大的优点可能在于性能,这对于建立高频电子交易系统尤其重要。内存映射文件通常比标准通过正常IO访问文件要快。另一个巨大的优势是内存映 射IO允许加载不能直接访问的潜在巨大文件 。经验表明,内存映射IO在大文件处理方面性能更加优异。尽管它也有不足——增加了页面错误的数目。由于操作系统只将一部分文件加载到内存,如果一个请求 页面没有在内存中,它将导致页面错误。同样它可以被用来在两个进程*享数据。
支持内存映射IO的操作系统
大多数主流操作系统比如Windows平台,UNIX,Solaris和其他类UNIX操作系统都支持内存映射IO和64位架构,你几乎可以将所有文件映射到内存并通过JAVA编程语言直接访问。
Java的内存映射IO的要点
如下为一些你需要了解的java内存映射要点:
java通过java.nio包来支持内存映射IO。
内存映射文件主要用于性能敏感的应用,例如高频电子交易平台。
通过使用内存映射IO,你可以将大文件加载到内存。
内存映射文件可能导致页面请求错误,如果请求页面不在内存中的话。
映射文件区域的能力取决于于内存寻址的大小。在32位机器中,你不能访问超过4GB或2 ^ 32(以上的文件)。
内存映射IO比起Java中的IO流要快的多。
加载文件所使用的内存是Java堆区之外,并驻留共享内存,允许两个不同进程共享文件。
内存映射文件读写由操作系统完成,所以即使在将内容写入内存后java程序崩溃了,它将仍然会将它写入文件直到操作系统恢复。
出于性能考虑,推荐使用直接字节缓冲而不是非直接缓冲。
不要频繁调用MappedByteBuffer.force()方法,这个方法意味着强制操作系统将内存中的内容写入磁盘,所以如果你每次写入内存映射文件都调用force()方法,你将不会体会到使用映射字节缓冲的好处,相反,它(的性能)将类似于磁盘IO的性能。
万一发生了电源故障或主机故障,将会有很小的机率发生内存映射文件没有写入到磁盘,这意味着你可能会丢失关键数据。
二、实例代码
1、传统IO读取数据,不指定缓冲区大小
- /**
- * 传统IO读取数据,不指定缓冲区大小
- * @author linbingwen
- * @since 2015年9月5日
- * @param path
- * @return
- */
- public static void readFile1(String path) {
- long start = System.currentTimeMillis();//开始时间
- File file = new File(path);
- if (file.isFile()) {
- BufferedReader bufferedReader = null;
- FileReader fileReader = null;
- try {
- fileReader = new FileReader(file);
- bufferedReader = new BufferedReader(fileReader);
- String line = bufferedReader.readLine();
- System.out.println("========================== 传统IO读取数据,使用虚拟机堆内存 ==========================");
- while (line != null) { //按行读数据
- System.out.println(line);
- line = bufferedReader.readLine();
- }
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- //最后一定要关闭
- try {
- fileReader.close();
- bufferedReader.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- long end = System.currentTimeMillis();//结束时间
- System.out.println("传统IO读取数据,不指定缓冲区大小,总共耗时:"+(end - start)+"ms");
- }
- }
- }
2、传统IO读取数据,指定缓冲区大小
- /**
- * 传统IO读取数据,指定缓冲区大小
- * @author linbingwen
- * @since 2015年9月5日
- * @param path
- * @return
- * @throws FileNotFoundException
- */
- public static void readFile2(String path) throws FileNotFoundException {
- long start = System.currentTimeMillis();//开始时间
- int bufSize = 1024 * 1024 * 5;//5M缓冲区
- File fin = new File(path); // 文件大小200M
- FileChannel fcin = new RandomAccessFile(fin, "r").getChannel();
- ByteBuffer rBuffer = ByteBuffer.allocate(bufSize);
- String enterStr = "\n";
- long len = 0L;
- try {
- byte[] bs = new byte[bufSize];
- String tempString = null;
- while (fcin.read(rBuffer) != -1) {//每次读5M到缓冲区
- int rSize = rBuffer.position();
- rBuffer.rewind();
- rBuffer.get(bs);//将缓冲区数据读到数组中
- rBuffer.clear();//清除缓冲
- tempString = new String(bs, 0, rSize);
- int fromIndex = 0;//缓冲区起始
- int endIndex = 0;//缓冲区结束
- //按行读缓冲区数据
- while ((endIndex = tempString.indexOf(enterStr, fromIndex)) != -1) {
- String line = tempString.substring(fromIndex, endIndex);//转换一行
- System.out.print(line);
- fromIndex = endIndex + 1;
- }
- }
- long end = System.currentTimeMillis();//结束时间
- System.out.println("传统IO读取数据,指定缓冲区大小,总共耗时:"+(end - start)+"ms");
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
3、内存映射读文件
- /**
- * NIO 内存映射读大文件
- * @author linbingwen
- * @since 2015年9月15日
- * @param path
- */
- public static void readFile3(String path) {
- long start = System.currentTimeMillis();//开始时间
- long fileLength = 0;
- final int BUFFER_SIZE = 0x300000;// 3M的缓冲
- File file = new File(path);
- fileLength = file.length();
- try {
- MappedByteBuffer inputBuffer = new RandomAccessFile(file, "r").getChannel().map(FileChannel.MapMode.READ_ONLY, 0, fileLength);// 读取大文件
- byte[] dst = new byte[BUFFER_SIZE];// 每次读出3M的内容
- for (int offset = 0; offset < fileLength; offset += BUFFER_SIZE) {
- if (fileLength - offset >= BUFFER_SIZE) {
- for (int i = 0; i < BUFFER_SIZE; i++)
- dst[i] = inputBuffer.get(offset + i);
- } else {
- for (int i = 0; i < fileLength - offset; i++)
- dst[i] = inputBuffer.get(offset + i);
- }
- // 将得到的3M内容给Scanner,这里的XXX是指Scanner解析的分隔符
- Scanner scan = new Scanner(new ByteArrayInputStream(dst)).useDelimiter(" ");
- while (scan.hasNext()) {
- // 这里为对读取文本解析的方法
- System.out.print(scan.next() + " ");
- }
- scan.close();
- }
- System.out.println();
- long end = System.currentTimeMillis();//结束时间
- System.out.println("NIO 内存映射读大文件,总共耗时:"+(end - start)+"ms");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
三、测试对比
1、100M文件
文件大小如下:
调用如下:
- public static void main(String args[]) {
- String path = "D:" + File.separator + "CES_T_MSM_LIQ-TRANS-ESP_20150702_01.DAT";
- readFile1(path);
- //readFile2(path);
- //readFile3(path);
- }
(1)传统IO读取数据,不指定缓冲区大小,总共耗时:80264ms
其内存使用如下:
(2)传统IO读取数据,指定缓冲区大小,总共耗时:80612ms
其内存使用如下:
(3)NIO 内存映射读大文件,总共耗时:90955ms
其内存使用如下:
分析发现内存映射并没有比传统IO快多少,甚至还更加慢了,有可能是因为磁盘IO操作多了,反而降低了其效率,内存映射看来还是对大文件比较有好的效果。小文件基本上是没有多大的差别的。
2、1.2G文件
传统IO读取数据,不指定缓冲区大小,总共耗时:1245111ms
NIO 内存映射读大文件,总共耗时:1223877ms(大概20分钟多点)