Java NIO之零拷贝
传统零拷贝
传统的零拷贝指的是数据传输过程中,不需要CPU进行数据的拷贝。主要是数据在用户空间与内核中间之间的拷贝。
比如,从磁盘文件读取数据流,然后通过Socket发送数据流。
传统方法需要 4 次数据拷贝和 4 次上下文切换:
- 数据从磁盘读取到内核的read buffer
- 数据从内核缓冲区拷贝到用户缓冲区
- 数据从用户缓冲区拷贝到内核的socket buffer
- 数据从内核的socket buffer拷贝到网卡接口的缓冲区
明显 2、3 两步是不必要的,通过java的Filechannel.transferTo和TransferFrom方法,可以避免这两次多余的拷贝(需要操作系统提供支持)。
- 调用transferTo,数据从文件由DMA引擎拷贝到内核read buffer
- 接着DMA从内核read buffer将数据拷贝到网卡接口buffer
上述两次操作都不需要CPU参与,所以就达到了零拷贝。
上述操作的关键点在于,我们平时的read/write,都会在I/O设备与应用程序空间之间经历一个【内核缓冲区->用户缓冲区(内存)】的过程,省去了这个拷贝过程,就减少了很多不必要的内存I/O操作。
Netty中的零拷贝
Netty也用到了上述的操作系统级的零拷贝,除此之外,在ByteBuf的实现上,Netty也提供了零拷贝的一些实现。Netty的零拷贝体现在三个方面:
- Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
- Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
- Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。
注1:Direct Buffer是在Java堆外分配的内存,不直接受GC管理,但它的JAVA对象是归GC管理的,只有当GC回收了它的JAVA对象,操作系统才会释放Direct Buffer所申请的空间。
注2:我们知道,JVM的GC会整理Java堆内存,这个操作会对内存对象进行拷贝移动,造成对象的内存地址变动。而我们想要让操作系统将数据通过网络接口发送,需要告诉操作系统我们数据的内存地址,并且在操作系统发送完数据之前,这个地址是不能动的。这就要求我们必须先把数据放到一块不会被GC移动的内存区域中,然后再将这块区域的内存地址发给操作系统。JDK源码中的NIO操作就是这么实现的(sun.nio.ch.IOUtil#read和sun.nio.ch.IOUtil#write方法),如果数据源不是一个DirectBuffer对象,就会先获取一个临时的DirectBuffer对象,把数据放进去,然后再执行真正的操作。
参考:
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 using1174@foxmail.com
文章标题: Java NIO之零拷贝
文章字数: 924
本文作者: Jun
发布时间: 2019-09-11, 20:23:00
最后更新: 2019-09-11, 21:55:16
原始链接: http://yoursite.com/2019/09/11/Java-NIO之零拷贝/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。