零拷贝是网络编程的关键,很多性能优化都离不开它。零拷贝是指:从操作系统的角度来看,文件的传输不存在CPU的拷贝,只存在DMA拷贝。在Java程序中,常用的零拷贝有 mmap(内存映射)和 sendFile

零拷贝不仅仅带来更少的数据复制,还能减少线程的上下文切换,减少CPU缓存伪共享以及无CPU校验和计算。

传统IO的读写

3次切换,4次拷贝

  1. File file = new File("test.txt");
  2. RandomAccessFile raf = new RandomAccessFile(file, "rw");
  3. byte[] arr = new byte[(int) file.length()];
  4. raf.read(arr);
  5. Socket socket = new ServerSocket(8080).accept();
  6. socket.getOutputStream().write(arr);

NIO的零拷贝 - 图1
DMA:直接内存拷贝,即不使用CPU来完成拷贝。

mmap优化的IO读写

mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内存空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。需要进行3次上下文切换,3次数据拷贝。适合小数据量的读写。
NIO的零拷贝 - 图2

sendFile优化的IO读写

Linux2.1 版本提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换。需要2次上下文切换和3此数据拷贝。适合大文件的传输。
NIO的零拷贝 - 图3

而 Linux 在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket Buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。2次上下文切换和2此数据拷贝
NIO的零拷贝 - 图4

注:这里其实有一次CPU拷贝,kernel buffer -> socket buffer。但是,拷贝的信息很少,只拷贝了数据的长度、偏移量等关键信息,消耗低,可以忽略不计。

NIO中的零拷贝(transferTo)demo

  1. public static void main(String[] args) throws IOException {
  2. SocketChannel socketChannel = SocketChannel.open();
  3. socketChannel.connect(new InetSocketAddress("localhost", 7001));
  4. //得到一个文件CHANNEl
  5. FileChannel channel = new FileInputStream("a.zip").getChannel();
  6. //准备发送
  7. long startTime = System.currentTimeMillis();
  8. //在Linux下一个 transferTo 方法就可以完成传输
  9. //在windows 下一次调用 transferTo 只能发送 8M,就需要分段传输文件
  10. //传输时的位置
  11. //transferTo 底层使用到零拷贝
  12. long transferCount = channel.transferTo(0, channel.size(), socketChannel);
  13. System.out.println("发送的总的字节数:" + transferCount + " 耗时:" + (System.currentTimeMillis() - startTime));
  14. channel.close();
  15. }