Java NIO(一)从操作系统角度对比IO与NIO的对比

传统IO

先来看一下FileInputStream调用read()方法后,底层都做了什么操作。

FileInputStream in = new FileInputStream(file);
byte bytes[] = new byte[1024];
in.read(contentByte);
  1. 内核发送一条命令给磁盘控制器,告诉磁盘控制器,我要读取磁盘上的数据。
  2. 利用DMA把磁盘上的数据读取到内核空间的缓冲区。
  3. 内核空间把数据读取到用户空间的缓冲区(上面例子中bytes就是用户空间的缓冲区)。

这里写图片描述

上图中的概念解释:

  1. User space是用户空间,Kernel space是内核空间。用户空间就是用户进程所在的内存区域;系统空间就是操作系统占据的内存区域。用户进程和系统进程的所有数据都在相应的内存空间中。(这里面提到的内存都是虚拟内存)
  2. buffer是缓冲区,在用户空间和内核空间分别被提到。内核缓冲区:当进程需要操作的磁盘数据时,先到内核缓冲区去看看数据是否在内核缓冲区,如果没有,内核把读取磁盘中此数据的请求添加到内核的请求队列,然后挂起此进程,等解决了别的进程的问题之后,磁盘中的数据也读取到了内核缓冲区,然后复制数据到进程缓冲区,接着唤醒这个被挂起的进程,进程从自己的进程缓冲区中拿到数据,然后继续工作。
  3. Disk Controller是磁盘控制器,主板上对硬盘/软盘进行控制、资源分配、数据输入输出调节的特定的电路芯片。我理解的是软件与磁盘之间的数据传输枢纽,内核只有通过磁盘控制器才能操作磁盘上的数据。

综上可以发现:利用Java的IO的read方法在读取磁盘数据时,经历了将磁盘数据拷贝到内核缓冲区,又被拷贝到用户缓冲区。经历了两次拷贝,当然会影响程序执行速度。

DMA为什么不直接将磁盘上的数据读入到用户缓冲区呢?是因为:

  • 内核缓冲区作为一个中间缓冲区。用来“适配”用户缓冲区的“任意大小”和每次读磁盘块的固定大小。
  • 用户缓冲区位于用户态空间,而DMA读取数据这种操作涉及到底层的硬件,硬件是不能直接访问用户态空间的。

JAVA NIO内存映射文件

JAVA NIO克服了上述的缺陷。NIO相对于IO的改进如下图所示:
这里写图片描述

关于上图中的解释:

  • 左图中用户空间的buffer和内核空间的buffer都是虚拟内存(有关于虚拟内存与物理内存参考我的另外一篇博客:C语言——操作系统内存分配过程)中的一部分,分别对应着不同区域的物理内存。
  • 右图是NIO在读取磁盘上的文件时的过程:
    1.用户空间的缓冲区与内控空间的缓冲区映射到同一个物理内存上。
    2.使用文件系统建立从用户空间直到可用文件系统页的虚拟内存映射。
    3.当用户进程访问“内存映射文件”地址时,自动产生缺页错误,然后由底层的OS负责将磁盘上的数据送到内存。

操作系统的缺页错误处理:

当处理器要处理的内存页不在内存中,那么处理器会产生一个缺页异常。操作系统会进行如下操作:

  • 将要访问的页从磁盘复制到内存,内存不够时,将内存中不经常用的页移到磁盘。
  • 缺页异常返回。
  • 重新执行刚才要处理的页。

IO、NIO通道、NIO映射文件三种方式读取文件的速度对比

传统io读取文件:

public static void testFileStream(String path)throws IOException {
        FileInputStream in = new FileInputStream(path);
        byte bytes[] = new byte[1024];
        long start = System.currentTimeMillis();//开始时间
        while (in.read(bytes) >0){
            bytes = new byte[1024];
        }
        in.close();
        long end = System.currentTimeMillis();//结束时间
        System.out.println("Read time: " + (end - start) + "ms");
    }

FileChannel读取文件:

 public static void testFileChannel(String path) throws IOException {
        RandomAccessFile file = new RandomAccessFile(path, "rw");
            FileChannel channel = file.getChannel();
            ByteBuffer buff = ByteBuffer.allocate(1024);
            long start = System.currentTimeMillis();//开始时间
            while (channel.read(buff) != -1) {
                buff.flip();
                buff.clear();
            }
            long end = System.currentTimeMillis();
            System.out.println("Read time: " + (end - start) + "ms");
    }

NIO利用内存映射文件的方式读取文件:

public static void testMappedByteBuffer(String path) throws IOException {
            RandomAccessFile file = new RandomAccessFile(path, "rw");
            FileChannel fc = file.getChannel();
            int len = (int) file.length();
            MappedByteBuffer buffer = fc.map(FileChannel.MapMode.READ_ONLY, 0, len);
            byte[] b = new byte[1024];

            long start = System.currentTimeMillis();
            for (int offset = 0; offset < len; offset += 1024) {

                if (len - offset > 1024) {
                    buffer.get(b);
                } else {
                    buffer.get(new byte[len - offset]);
                }
            }
            long end = System.currentTimeMillis();

            System.out.println("Read time: " + (end - start) + "ms");
    }

测试:

public static void main(String[] args) throws IOException {

        String path = "/home/xyh/test/xiaoma.zip";

        testFileStream(path);//传统io
        testFileChannel(path);//nio通道
        testMappedByteBuffer(path);//nio内存映射文件
    }

相同的程序执行了2次,第一次结果如下:

  • Read time: 2074ms
  • Read time: 235ms
  • Read time: 31ms

看到上面的执行结果,我很奇怪,为什么io的执行速度要比FileChannel慢那么多,底层的执行流程都是一样的,于是我重新执行了上面的程序,第二次结果如下:

  • Read time: 232ms
  • Read time: 243ms
  • Read time: 40ms

这个结果是比较符合预期的,原因是,io和FileChannel在操作磁盘上文件时,第一次操作都会将磁盘中的数据拷贝到用户空间的缓存中(正好验证了文章一开始提到的传统IO调用read方法的执行过程),之后的每一次读取数据都是检查缓冲区中是否存在要读取的数据,如果存在直接读取。

本文的思路来源和图来源于这位不知名的大神JAVA IO 以及 NIO 理解

猜你喜欢

转载自blog.csdn.net/xyh930929/article/details/80900246