零拷贝字面意思是数据不需要来回拷贝,大大提高了系统的性能。图片来自Pexels。我们在JavaNIO、Netty、Kafka、RocketMQ等框架中经常听到零拷贝,它常被作为其性能提升的亮点;下面从I/O的几个概念入手,然后分析零拷贝。I/O概念Buffer缓冲区是所有I/O的基础。I/O只不过是将数据移入或移出缓冲区;当进程执行I/O操作时,它会向操作系统发送请求,要求要么清空缓冲区中的数据(写入),要么填充缓冲区(读取)。我们来看一个Java进程发起Read请求加载数据的大致流程:进程发起Read请求后,内核收到Read请求后首先会检查进程需要的数据是否已经存在于内核空间。数据复制到进程的缓冲区。如果没有内核然后向磁盘控制器发送命令从磁盘读取数据,磁盘控制器将数据直接写入内核Readbuffer,这是由DMA完成的。接下来,内核将数据复制到进程的缓冲区;如果进程发起Write请求,还需要将userbuffer中的数据复制到内核的socketbuffer中,然后通过DMA将数据复制到网卡中发送出去。您可能认为这是对空间的浪费。每次都需要将内核空间的数据拷贝到用户空间,所以零拷贝的出现就是为了解决这个问题。提供零拷贝的方式有两种:mmap+writeSendfile虚拟内存所有现代操作系统都使用虚拟内存,使用虚拟地址代替物理地址。这样做的好处是:多个虚拟地址可以指向同一个物理地址内存地址。虚拟内存空间可以大于可用的实际物理地址。第一个特性允许内核空间地址和用户空间虚拟地址映射到相同的物理地址,这样DMA就可以填充对内核和用户空间进程都可见的缓冲区。大致如下图所示:省略了内核和用户空间之间的拷贝,Java也利用了操作系统的这一特性来提高性能。下面重点说说Java对零拷贝的支持。mmap+write方式使用mmap+write方式代替原来的read+write方式。mmap是一种内存映射文件方法,将一个文件或其他对象映射到进程的地址空间,实现文件磁盘地址和进程的虚拟地址空间。虚拟地址之间一一对应。这样一来,原来内核Readbuffer拷贝数据到userbuffer就可以省下来了,但是内核Readbuffer还是需要拷贝数据到内核Socketbuffer。大致如下图所示:Sendfile方式Sendfile系统调用是内核2.1版本引入的,目的是简化网络上两个通道之间的数据传输过程。Sendfile系统调用的引入,不仅减少了数据的拷贝,也减少了上下文切换的次数,大致如下图所示:数据传输只发生在内核空间,所以减少了一次上下文切换;但是还有一个Copy,这次能不能把Copy也省掉?Linux2.4内核做了改进,将Kernelbuffer中对应的数据描述信息(内存地址,偏移量)记录到对应的Socketbuffer中,这样内核空间中的一次CPUCopy也省去了。Java零拷贝MappedByteBufferJavaNIO提供的FileChannel提供了map()方法,可以在打开的文件和MappedByteBuffer之间建立虚拟内存映射。MappedByteBuffer继承自ByteBuffer,类似于基于内存的缓冲区,只是对象的数据元素存储在磁盘上的文件中。调用get()方法会从磁盘中获取数据,反映文件当前的内容,调用put()方法会更新磁盘上的文件,对文件所做的修改也会对其他人可见读者。下面看一个简单的读取例子,然后分析一下MappedByteBuffer:byte[]ds=newbyte[(int)len];MappedByteBuffermappedByteBuffer=newFileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY,0,len);for(intoffset=0;offset
