所谓的【零拷贝】,并不是真正无拷贝,而是不会拷贝数据到用户态(从Java角度,就是到jvm内存中)。
需求
将一个文件通过网络写出(将一个文件发送给网络中的其他电脑)。
传统IO
- Java本身并不具备从硬件读写(IO)能力,因此read方法调用后,要从Java程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用DMA(Direct Memory Access)来实现文件读,其间也不会使用CPU;
DMA可以理解为硬件单元,用来解放CPU完成文件IO
- 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(既byte[]),这期间CPU会参与拷贝,无法利用DMA;
- 调用write方法,这时将数据从用户缓冲区写入socket缓冲区,CPU会参与拷贝;
- 接下来要向网卡写数据,这项能力Java不具备(直接操作硬件),因此又得从用户态切换到内核态,调用操作系统的写能力,使用DMA将socket缓冲区的数据写入网卡,不会使用cpu;
小结:
- 用户态与内核态的切换发生了3次
-
- 数据拷贝1:用户态切换到内核态
- 数据拷贝2:内核态切换用户态
- 数据拷贝4:用户态切换到内核态
- 数据拷贝共4次
NIO优化
NIO是JDK1.4引入的。
Linux2.1
提供了sendFile方法,Java中对应着两个channel调用transferTo/tansferFrom方法拷贝数据。
- Java调用transferTo方法后,要从Java程序的用户态切换至内核态,使用DMA将数据读入内核缓冲区,不会使用CPU
- 数据从内核缓冲区传输到socket缓冲区,CPU会参与拷贝
- 最后使用DMA将socket缓冲区的数据写入网卡,不会使用CPU
小结:
- 只发生了一次用户态与内核态的切换
- 数据拷贝发生了3次
Linux2.4
- Java调用transferTo方法后,要从Java程序的用户态切换至内核态,使用DM将数据读入内核缓冲区,不会使用CPU
- 只会将一些offset和length信息拷入socket缓冲区,几乎无消耗
- 使用DMA将内核缓冲区的数据写入网卡,不会使用CPU
小结:
- 只发生了1次用户态与内核态的切换
- 数据拷贝发生了2次
总结
- 更少的用户态与内核态的切换
- 不利用CPU计算,减少CPU缓存伪共享
- 零拷贝适合小文件传输
Netty中的零拷贝
Netty中所谓的零拷贝,实际就是ByteBuf类中的slice()方法生成一个新的ByteBuf对象,与原对象共用底层的数据,但是有独立的readIndex,writeIndex等。这样就可以进行单独的读操作。(拷贝的对象,不要进行写操作,因为共用,所以写操作会影响原始对象对应的数据)
Netty中使用直接内存,内核缓冲区和用户缓冲区可以是同一个,减少了一次数据拷贝。
AIO(Netty废弃5.x)
AIO 用来解决数据复制阶段的阻塞问题
- 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
- 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果
异步模型需要底层操作系统(Kernel)提供支持
- Windows 系统通过 IOCP 实现了真正的异步 IO
- Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势
Netty5.x废弃原因(同Linux方式实现AIO):
- 性能没有优势;
- 引入不必要的复杂性
- 维护成本增加