【Java】Java中的零拷贝( 二 )


  1. 底层发起JNI调用,创建堆外缓冲区;
  2. JNI中发起read系统调用,此时需要由用户空间切换到内核空间;
  3. 进入到内核空间,DMA读取文件数据到内核缓冲区(DMA拷贝);
  4. 将内核缓冲区的数据拷贝到用户缓冲区(CPU拷贝),切换回用户空间;
  5. 将堆外缓冲区的数据拷贝到JVM堆内缓冲区中(CPU拷贝);

【Java】Java中的零拷贝

文章插图
在Java的NIO中,提供了DirectByteBuffer , 可以直接分配堆外内存,减少了一次从堆外内存到堆内内存的复制(CPU复制):
【Java】Java中的零拷贝

文章插图
直接I/O缓存I/O经过了Page Cache,读取过程中需要将数据从Page Cache的缓冲区中拷贝到用户空间的缓存区,那么有没有一种方式可以省去这个拷贝的过程?
答案是有的,那就是直接I/O,应用程序直接访问磁盘数据 , 绕过了Page Cache,省去了从内核缓冲区拷贝到用户缓冲区的过程:
【Java】Java中的零拷贝

文章插图
目前JAVA并没有原生的直接/O操作方式,不过公众号博主Kirito提供了在JAVA中进行直接I/O操作的方法,具体参见【Kirito的技术分享】Java 文件 IO 操作之 DirectIO 。
内存映射内存映射就是将虚拟空间地址映射到物理空间地址 , 每个进程维护了一张页表 , 记录虚拟地址和物理地址之间的映射关系 , 当进程访问的虚拟地址在页表中无法查到映射关系时 , 系统产生缺页异常,进入内核空间为虚拟地址分配物理内存,并更新页表 , 记录映射关系 。
文件映射
内存映射除了映射虚拟空间地址和物理空间地址,还包括将磁盘的文件内容映射到虚拟地址空间,称为文件映射,此时可以通过访问内存来访问文件里面的数据。
mmap系统调用可以将文件映射到虚拟内存空间 。文件映射的流程如下:
  1. 进行mmap系统调用,将文件和虚拟地址空间建立映射,注意此时还没有分配物理内存空间 , 只是在逻辑上建立了虚拟地址和文件之间的映射关系,物理内存只有真正使用的时候才会分配 。
  2. 应用程序访问用户空间虚拟内存中的某个地址,发现无法在页表中查到数据 , 产生缺页异常 , 此时进入内核空间
  3. 因为不能直接使用物理地址,所以需要使用内核的虚拟地址临时建立与物理内存的映射关系,将文件内容读取到物理内存中,待数据读取完毕之后取消临时映射即可 。
  4. 缺页异常处理完毕,物理内存中已经加载了文件的数据,此时用户空间就可以通过虚拟地址直接访问物理内存中映射的文件数据 。

【Java】Java中的零拷贝

文章插图
从文件映射的流程中可以看出它与缓存I/O相比 , 少了从内核缓冲区将数据拷贝到用户缓冲区的步骤,减少了一次拷贝 。
Java NIO中提供了MappedByteBuffer来处理文件映射,下面是一个读取文件的例子:
public class MappedByteBufferTest {public static void main(String[] args) {try (RandomAccessFile file = new RandomAccessFile(new File("/Users/sml/test.txt"), "r")) {// 获取FileChannelFileChannel fileChannel = file.getChannel();long size = fileChannel.size();// 调用map方法进行文件映射,返回MappedByteBufferMappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);byte[] bytes = new byte[(int)size];for (int i = 0; i < size; i++) {// 读取数据bytes[i] = mappedByteBuffer.get();}} catch (Exception e) {e.printStackTrace();}}}零拷贝零拷贝一般指的是从磁盘读取文件发送到网络或者从网络接收数据写入到磁盘文件的过程中,减少数据的拷贝次数 。
网络I/O
网络I/O与网络数据发送/接收有关,与文件I/O的底层原理一致,同样以读取数据为例,文件I/O是从磁盘读取文件,网络I/O是从网卡中读取数据 。比如客户端与服务端建立了一个连接,客户端向服务端发送数据 , 服务端从网卡中读取客户端发送的数据到内核中的socket缓冲区,再将socket缓冲区的数据复制到用户空间的缓冲区:
【Java】Java中的零拷贝

文章插图

推荐阅读