1. 前言

最近研究Java中内存映射I/O。Java类库中的NIO中的内存映射文件MappedByteBuffer,相对于Java I/O是一个新的功能。特把适合用于处理大文件,在对大文件处理的时候效率极高。本文章将从操作系统I/O调用原理讲解为什么内存映射文件MappedByteBuffer相比较Java I/O性能极高。话不多说,我们开始学习吧。

2. 浅谈Java I/O InputStream Read调用

在Java I/O InputStream中我们可以调用Java API中Read、Write函数进行读取数据流(文件流、网络Socket流)。其实我们从源码中可以分析出来,InputStream(比如FileInputStream)对于Java API Read和Write函数的调用,最终是调用JNI(Java Native Interface)的read、write系统调用。JNI,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。最终调用操作系统底层系统调用。

在传统的文件IO操作中,我们都是调用操作系统提供的底层标准IO系统调用函数 read()、write() ,此时调用此函数的进程(在JAVA中即java进程)由当前的用户态切换到内核态,然后OS的内核代码负责将相应的文件数据读取到内核的IO缓冲区,然后再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,也就是拷贝到了用户缓冲区,这样便完成了一次IO操作。

至于为什么要多此一举搞一个内核IO缓冲区把原本只需一次拷贝数据的事情搞成需要2次数据拷贝呢? 我想学过操作系统或者计算机系统结构的人都知道,这么做是为了减少磁盘的IO操作,为了提高性能而考虑的,因为我们的程序访问一般都带有局部性,也就是所谓的局部性原理,在这里主要是指的空间局部性,即我们访问了文件的某一段数据,那么接下去很可能还会访问接下去的一段数据,由于磁盘IO操作的速度比直接访问内存慢了好几个数量级,所以OS根据局部性原理会在一次 read()系统调用过程中预读更多的文件数据缓存在内核IO缓冲区中,当继续访问的文件数据在缓冲区中时便直接拷贝数据到进程私有空间,避免了再次的低效率磁盘IO操作。在JAVA中当我们采用IO包下的文件操作流,如:

FileInputStream in = new FileInputStream("D:\\java.txt");
in.read();

但是这种I/O调用存在频繁操作系统调用,并且每次操作系统调用需要从用户态切换到内核态的问题,因为我们需要不停的read数据,就要不停的调用操作系统底层函数read()、write()函数。为了解决这个问题,Java I/O提供了一种BufferedInputStream这种装饰器类,这个类中会有一段事先缓存的byte[]数据,这样一来,我们每一次 buf_in.read() 时候,BufferedInputStream 会根据情况自动为我们预读更多的字节数据到它自己维护的一个内部字节数组缓冲区中,这样我们便可以减少系统调用次数,相应减少了用户态程序切换到内核态,从而达到其缓冲区的目的。

FileInputStream in = new FileInputStream("D:\\java.txt");
BufferedInputStream buf_in = new BufferedInputStream(in);
buf_in.read();
public
class BufferedInputStream extends FilterInputStream {  private static int defaultBufferSize = 8192;  /** * The internal buffer array where the data is stored. When necessary, * it may be replaced by another array of * a different size. */  protected volatile byte buf[];  /** * See * the general contract of the <code>read</code> * method of <code>InputStream</code>. * * @return     the next byte of data, or <code>-1</code> if the end of the *             stream is reached. * @exception  IOException  if this input stream has been closed by *              invoking its {@link #close()} method, *              or an I/O error occurs.  * @see        java.io.FilterInputStream#in */  public synchronized int read() throws IOException {  if (pos >= count) {  fill();  if (pos >= count)  return -1;  }  return getBufIfOpen()[pos++] & 0xff;  }

我们可以看到,BufferedInputStream 内部维护着一个 字节数组 byte[] buf 来实现缓冲区的功能,我们调用的 buf_in.read() 方法在返回数据之前有做一个 if 判断,如果 buf 数组的当前索引不在有效的索引范围之内,即 if 条件成立, buf 字段维护的缓冲区已经不够了,这时候会调用 内部的 fill() 方法进行填充,而fill()会预读更多的数据到 buf 数组缓冲区中去,然后再返回当前字节数据,如果 if 条件不成立便直接从 buf缓冲区数组返回数据了。其中getBufIfOpen()返回的就是 buf字段的引用。顺便说下,源码中的 buf 字段声明为 protected volatile byte buf[]; 主要是为了通过 volatile 关键字保证 buf数组在多线程并发环境中的内存可见性.

3. 内存映射文件

BufferedInputStream虽然解决了进程进行频繁的操作系统底层调用的问题(因为我们可以从缓冲区中读取数据,不用进行操作系统read调用),但是他还是不能解决数据从磁盘读取到内核缓冲区,然后内核缓冲区的数据复制移动到用户空间缓冲区。程序还是需要从用户态切换到内核态,然后再进行操作系统调用,并且数据移动和复制了两次。

Java NIO在此后采用了MappedByteBuffer用来解决这个,内存映射文件和之前说的 标准IO操作最大的不同之处就在于它虽然最终也是要从磁盘读取数据,但是它并不需要将数据读取到OS内核缓冲区,而是直接将进程的用户私有地址空间中的一部分区域与文件对象建立起映射关系,就好像直接从内存中读、写文件一样,速度当然快了。为了说清楚这个,我们以 Linux操作系统为例子,看下图:

内存映射IO原理:此图为 Linux 2.X 中的进程虚拟存储器,即进程的虚拟地址空间,如果你的机子是 32 位,那么就有 2^32 = 4G的虚拟地址空间,我们可以看到图中有一块区域: “Memory mapped region for shared libraries” ,这段区域就是在内存映射文件的时候将某一段的虚拟地址和文件对象的某一部分建立起映射关系,此时并没有拷贝数据到内存中去,而是当进程代码第一次引用这段代码内的虚拟地址时,触发了缺页异常,这时候OS根据映射关系直接将文件的相关部分数据拷贝到进程的用户私有空间中去,当有操作第N页数据的时候重复这样的OS页面调度程序操作。注意啦,原来内存映射文件的效率比标准IO高的重要原因就是因为少了把数据拷贝到OS内核缓冲区这一步,数据直接被拷贝到进程的地址空间中。

4. MpappedByteBuffer

java中提供了3种内存映射模式,即:只读(readonly)、读写(read_write)、专用(private) ,对于 只读模式来说,如果程序试图进行写操作,则会抛出ReadOnlyBufferException异常;第二种的读写模式表明了通过内存映射文件的方式写或修改文件内容的话是会立刻反映到磁盘文件中去的,别的进程如果共享了同一个映射文件,那么也会立即看到变化!而不是像标准IO那样每个进程有各自的内核缓冲区,比如JAVA代码中,没有执行 IO输出流的 flush() 或者 close() 操作,那么对文件的修改不会更新到磁盘去,除非进程运行结束;最后一种专用模式采用的是OS的“写时拷贝”原则,即在没有发生写操作的情况下,多个进程之间都是共享文件的同一块物理内存(进程各自的虚拟地址指向同一片物理地址),一旦某个进程进行写操作,那么将会把受影响的文件数据单独拷贝一份到进程的私有缓冲区中,不会反映到物理文件中去。

java中提供了3种内存映射模式,即:只读(readonly)、读写(read_write)、专用(private) ,对于 只读模式来说,如果程序试图进行写操作,则会抛出ReadOnlyBufferException异常;第二种的读写模式表明了通过内存映射文件的方式写或修改文件内容的话是会立刻反映到磁盘文件中去的,别的进程如果共享了同一个映射文件,那么也会立即看到变化!而不是像标准IO那样每个进程有各自的内核缓冲区,比如JAVA代码中,没有执行 IO输出流的 flush() 或者 close() 操作,那么对文件的修改不会更新到磁盘去,除非进程运行结束;最后一种专用模式采用的是OS的“写时拷贝”原则,即在没有发生写操作的情况下,多个进程之间都是共享文件的同一块物理内存(进程各自的虚拟地址指向同一片物理地址),一旦某个进程进行写操作,那么将会把受影响的文件数据单独拷贝一份到进程的私有缓冲区中,不会反映到物理文件中去。

在JAVA NIO中可以很容易的创建一块内存映射区域,代码如下:

File file = new File("E:\\download\\office2007pro.chs.ISO");
FileInputStream in = new FileInputStream(file);
FileChannel channel = in.getChannel();
MappedByteBuffer buff = channel.map(FileChannel.MapMode.READ_ONLY, 0,channel.size());

按照jdk文档的官方说法,内存映射文件属于JVM中的直接缓冲区,还可以通过 ByteBuffer.allocateDirect() ,即DirectMemory的方式来创建直接缓冲区。他们相比基础的 IO操作来说就是少了中间缓冲区的数据拷贝开销。同时他们属于JVM堆外内存,不受JVM堆内存大小的限制。

Java基础:JavaNIO 之 内存映射文件原理相关推荐

  1. 内存映射文件 写入 卡住_在Java中使用内存映射文件时检测(写入)失败

    内存映射文件 写入 卡住 内存映射文件是一个很好的并且经常被忽视的工具. 我不会在这里详细介绍它们的工作方式(使用 力 Google Luke!),但我将快速总结其优势: 操作系统提供的延迟加载和写入 ...

  2. 在Java中使用内存映射文件时检测(写入)失败

    内存映射文件是一个很好的并且经常被忽视的工具. 我不会在这里详细介绍它们的工作方式(使用 力 Google Luke!),但我将快速总结其优势: 操作系统提供的延迟加载和写入缓存(您不必自己编写,并且 ...

  3. 【java】java中内存映射文件和IO

    转载:https://leokongwq.github.io/2017/02/25/java-memorymapped-file-and-io.html 对大多数Java开发人员来说,Java中的内存 ...

  4. MongoDB内存映射文件

    2019独角兽企业重金招聘Python工程师标准>>> Mongodb源码分析--内存文件映射(MMAP) 内存映射文件原理探索 Linux 内存映射函数 mmap()函数详解 Li ...

  5. 【JavaNIO的深入研究4】内存映射文件I/O,大文件读写操作,Java nio之MappedByteBuffer,高效文件/内存映射...

    内存映射文件能让你创建和修改那些因为太大而无法放入内存的文件.有了内存映射文件,你就可以认为文件已经全部读进了内存,然后把它当成一个非常大的数组来访问.这种解决办法能大大简化修改文件的代码. file ...

  6. java 内存映射文件进程间通讯_[转]Windows环境下利用“共享内存”实现进程间通信的C/C++代码---利用CreateFileMapping和MapViewOfFile...

    进程间的通信方式有很多种, 上次我们说了最傻瓜的"共享外存/文件"的方法. 那么, 在本文中, 我们即将学习"共享内存"的方式实现进程间的通信, 这是IPC最快 ...

  7. java流与文件——内存映射文件

    [0]README 0.1) 本文描述转自 core java volume 2, 旨在理解 java流与文件--内存映射文件 的相关知识: 0.2)内存映射文件的目的是: 提高访问速度, 缓冲区Bu ...

  8. 内存映射文件mmap原理分析

    本文来说下内存映射文件 mmap 原理 文章目录 mmap原理分析 mmap原理分析 假设我们要把一个磁盘文件映射到内存里来,然后把映射到内存中的数据通过socket发送出去. 零拷贝有两种实现方式, ...

  9. Java NIO内存映射文件

    Java NIO 内存映射文件 文件操作的四大方法 效率对比 内存映射文件 缓冲区 缓冲区的获得: 文件加锁机制 文件锁使用注意点: 文件操作的四大方法 前提:内存的访问速度比磁盘高几个数量级,但是基 ...

最新文章

  1. 波士顿大学计算机科学硕士申请要求,波士顿大学计算机信息系统理学硕士研究生申请要求及申请材料要求清单...
  2. 只剩 1 天 | 神策 2019 数据驱动大会明天开幕
  3. 【机器学习基础】数学推导+纯Python实现机器学习算法26:随机森林
  4. 团队项目第一阶段冲刺站立会议06
  5. 使用 CliWrap 让C#中的命令行交互举重若轻
  6. 支付宝双11狂欢幕后的女程序员:服务全球12亿人,每天和不法分子打攻防战
  7. Memcache架构新思考
  8. 手机 — oppo手机录音放在哪个文件夹里
  9. 让ajax更加友好,实时显示后台处理进度。
  10. 你应该知道的十大常见黑客技术
  11. 电路中的正弦信号 Sin
  12. 【C语言学习】sscanf的简单使用.
  13. Linear algebra4---duality2
  14. 测试3d游戏pfs的软件,Beepa Fraps(3D游戏测帧录制软件)
  15. OpenCV——人脸识别模型训练(2)
  16. C语言程序设计--企业员工管理系统
  17. 无线专题 openwrt feeds、web框架luci(lua语言)、UCI (统一配置接口)
  18. 充电桩设计之电川 充电板的驱动控制程序
  19. 视频直播技术干货:一文读懂主流视频直播系统的推拉流架构、传输协议等
  20. 原厂HT7525.HT7530.HT7533.HT7536.HT7544.HT7550三端稳压管,性价比高

热门文章

  1. Object-C 打开工程,选择模拟起时,提示no scheme
  2. servlet配置web.xml问题
  3. docker学习笔记(四): 镜像和仓库
  4. 关于开发简易搜索引擎的一些总结和思考
  5. PHP - NetBeans中调试PHP
  6. 放上了一篇几个月前写的东西
  7. leetcode 160 简单难度 相交链表
  8. 扫脸支付引忧虑,那试试扫手支付?
  9. 获取和设置mfc中Edit输入的文字
  10. linux操作系统的特点包括什么,什么是Linux操作系统?有哪些主要特点?