NIO零拷贝的深入分析

简介: NIO零拷贝的深入分析

深入分析通过Socket进行数据文件传递中的传统IO的弊端以及NIO的零拷贝实现原理,及用户空间和内核空间的切换方式


传统的IO流程#




在这个过程中:

  1. 数据从磁盘拷贝进内核空间缓冲区
  2. 从内核空间缓冲区拷贝到用户空间缓冲区
  3. 从用户空间缓冲区拷贝回内核空间缓冲区
  4. 在从内核空间缓冲区拷贝到socket的缓冲区
  5. 由Socket缓存区传递给数据发送引擎发送


第三步的必要性:


IO操作涉及到本地方法,java担心,当使用native本地方法对堆内数组进行操作时发生GC, 因为堆内内存是受JVM影响的,一旦发生了垃圾回收机制就使得全部数据都是错乱的,而堆外内存是不受JVM控制的.


就这样, 前前后后一共发生了4次数据的拷贝,用户空间模式和内核空间模式来回切换了4次, 其中用户空间参与的第二次和第三次拷贝并没有对数据进行任何改动,它仅仅是起到了中转的作用; 这恰恰是传统的IO的局限性


NIO的零拷贝#



在NIO的数据传递模型中可以看到,用户明显少了用户空间缓冲区缓存数据的步骤, 减少了两次不必要的数据的拷贝,以及不必要的上下文切换, 具体如下:


  1. 数据从磁盘写入内核空间缓冲区
  2. 再从内核空间缓冲区写入到Socket缓冲区
  3. 由Socket缓存区传递给数据发送引擎发送


然而这个模型中仍然有问题存在,在内核空间缓冲区中仍然存在数据的拷贝


  • 数据从内核空间缓冲区拷贝进了Socket缓冲区


这种现状也是有办法解决的

在2.X版本的linux中,NIO的零拷贝模型如下:



这个模型中充分利用了Scatter/Gather 分散和汇聚的特性



这张图是最完美的零拷贝模型,


  1. 首先文件从磁盘中加载进内核空间缓冲区
  2. CPU将内核空间缓冲区存储的数据的adress以及数据的大小存放进Socket
  3. 协议引擎根据socket提供的数据的描述,直接去内核缓冲区取出数据


第2步 一个完整的可用的buffer被分散在两个buffer中, 可以理解成是一个分散的过程 Scatter

第3步 操作系统去收集buffer,可以理解成一个Gather的过程


从而实现了真正的零拷贝


回到Java#


除了上面的第一张图片以外,其他图片中数据全部在内核缓冲区,这部分空间对于人来说其实是一个黑盒,于是java提供了封装类帮我们和这块黑盒打交道


mappedByteBuffer#



这是他的继承体系,和HeadByteBuffer位于同一级,我们称它为内存映射文件 他是通道的调用map()方法得来的, 这个mappedByteBuffer相对于普通的buffer而言,他并没有板板整整的维护自己的数组,相反直接关联着堆外内存,针对它的任何修改,操作系统都会自动的同步到文件中


如下修改内存buffer,却更新了文件


RandomAccessFile randomAccessFile = new RandomAccessFile("123.txt", "rw");  //class sun.nio.ch.FileChannelImpl
FileChannel channel = randomAccessFile.getChannel();
System.out.println(channel.getClass());
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
// todo 接下来我们直接修改内存中的内容就行了,不需要修改文件
mappedByteBuffer.put(0, (byte) 'a');
mappedByteBuffer.put(3, (byte) 'b');
  randomAccessFile.close();
    channel.close();


关于FileChannel.MapMode文件通道的映射模型 是个枚举:

  • PRIVATE
  • READ_WRITE
  • READ_ONLY
    当我们想构建read_write类型的只能使用 RandomAccessFile类型的文件stream, 通过它的rw参数,设置为可读写的类型


关于ByteBufferByteBuffer.allocateDirect()


public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
    class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{
...
    }


最常用的ByteBuffer的allocateDirect()底层使用同样是MappedBytebuffer的实现类,DirectByteBuffer,这个对象相对于HeapByteBuffer来说,他并没有初始化父类ByteBuffer中的数组,但是它使用了超类BUffer中的Long类型的adress关键字

adress关键字的作用是 存放了一个堆外的地址,这个地址标记着一个堆外数组的位置,使得java可以使用unsafe类下的本地方法,操作adress标记的堆外内存,这样就省去了在第一张图片中的还要把堆内数组拷贝到堆外再进行读写的弊端,实现了零拷贝


scattering 和 gathering在NIO编程中的体现#


scattering是一个分散的过程,即把一整块数据分散在不同的buffer中,而gathering与之相反,是一个聚集的过程,只有搜集全所有的全部的buffer得到的数据才是有意义的

例子: 自定义网络协议 将请求头分装成多个缓存buffer中,实现了天然的解析


ByteBuffer[] byteBuffers = new ByteBuffer[3];
byteBuffers[0] = ByteBuffer.allocate(2);
byteBuffers[1] = ByteBuffer.allocate(3);
byteBuffers[2] = ByteBuffer.allocate(4);
 SocketChannel client = serverSocketChannel.accept();
 long read = client.read(byteBuffers);
相关文章
|
10月前
|
Java
Netty入门到超神系列-Java NIO零拷贝实战
这一章我们来操作一下NIO的零拷贝,这里我会先写代码样式一下传统IO数据拷贝场景下的耗时,然后再对比NIO场景下的考别耗时,通过耗时差异就能看到NIO零拷贝和传统IO拷贝的区别了。
84 0
|
4天前
|
存储 Java 数据处理
|
4天前
|
Java API
java中IO与NIO有什么不同
java中IO与NIO有什么不同
|
4天前
|
监控 Java
Java一分钟之-NIO:非阻塞IO操作
【5月更文挑战第14天】Java的NIO(New IO)解决了传统BIO在高并发下的低效问题,通过非阻塞方式提高性能。NIO涉及复杂的选择器和缓冲区管理,易出现线程、内存和中断处理的误区。要避免这些问题,可以使用如Netty的NIO库,谨慎设计并发策略,并建立标准异常处理。示例展示了简单NIO服务器,接收连接并发送欢迎消息。理解NIO工作原理和最佳实践,有助于构建高效网络应用。
8 2
|
4天前
|
缓存 Java API
Java NIO和IO之间的区别
NIO(New IO),这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
17 1
|
4天前
|
监控 Java 开发者
深入理解 Java 网络编程和 NIO
【4月更文挑战第19天】Java网络编程基于Socket,但NIO(非阻塞I/O)提升了效率和性能。NIO特点是非阻塞模式、选择器机制和缓冲区,适合高并发场景。使用NIO涉及通道、选择器和事件处理,优点是高并发、资源利用率和可扩展性,但复杂度、错误处理和性能调优是挑战。开发者应根据需求选择是否使用NIO,并深入理解其原理。
|
4天前
|
存储 监控 Java
浅谈Java NIO
浅谈Java NIO
7 0
|
4天前
|
消息中间件 存储 Java
【Java NIO】那NIO为什么速度快?
是这样的,在NIO零拷贝出现之前,一个I/O操作会将同一份数据进行多次拷贝。可以看下图,一次I/O操作对数据进行了四次复制,同时来伴随两次内核态和用户态的上下文切换,众所周知上下文切换是很耗费性能的操作。
31 1
【Java NIO】那NIO为什么速度快?
|
4天前
|
存储 监控 Java
Java输入输出:什么是NIO(New I/O)?
Java NIO是一种高效I/O库,特征包括非阻塞性操作、通道(如文件、网络连接)、缓冲区和选择器。选择器监控通道状态变化,通知应用程序数据可读写,避免轮询,提升性能。示例代码展示了一个使用NIO的服务器,监听连接、读取数据并处理客户端通信。
14 1