看了许多关于零拷贝的文章,感觉还是不太好理解,在这里对零拷贝进行归纳和总结。
首先需要明白零拷贝其实是进行三个层面的优化:
首先从操作系统层面来说,是避免了数据流经用户态。学习操作系统我们可以知道有用户态和内核态两个状态,核心是为了避免用户操作内核数据,用户通过系统调用的方式对I/O等硬件设备进行操作。因此使用java的输入输出流时,需要经过下列四步:
这样的流程需要经过四次上下文切换和四次拷贝
磁盘->内核缓冲区->用户缓冲区->网卡,流程如下图
对于netty的优化,其实很多文章都提到了dma,但是并没有讲清楚,其实我认为是必要重要而且许多对于Linux不了解的人需要了解一下的
DMA(Direct Memory Access,直接内存存取)
一般来说,计算机对内存数据进行处理的时候,需要从内存把数据读进寄存器,然后进行进一步的操作(比如运算处理)
但是有些数据并不需要运算处理这一类型的操作,只是单纯的移动数据,而把数据读进寄存器,然后再把数据从寄存器写进内存会消耗cpu资源,当需要读写大量数据的时候更是如此,DMA技术就很好地解决了这一问题
File.read(bytes) Socket.send(bytes)
因此,通过dma,我们可以方便的将磁盘中数据读取到内核缓冲区后,直接拷贝到网卡,这样就避免了一次拷贝和用户态/内核态的切换
这里使用到了FileRegion类的transferTo()方法,我们可以不提供buffer完成整个文件的发送,不再需要开辟buffer循环读写。但是在这个过程中无法对数据进行处理,也是一个缺点。
在进行io操作时,我们可以参考FileInputStream read方法的实现
private native int read0() throws IOException;
我们可以看到这是一个native方法,是由c语言来实现的,在这个过程中,会先在JVM Heap中allocate一段内存作为一次read最终返回的缓冲区,将外设数据读取到内核空间的缓冲区,待数据包准备好之后将之拷贝到用户空间的缓冲区——可以视为C Heap区域,最后将C Heap的buffer再拷贝回java内存中。这其实也与linux系统的i/o模型有关。
数据流动:磁盘->c堆内存->java内存
为什么需要这样呢?
在Linux系统中,与I/O相关的read()和write()系统调用,都需要传入一个指向你在程序中分配的一片内存区域起始地址的指针,然后操作系统会将数据填入这片区域或者从这片区域中读出数据。这里如果直接使用JVM堆中对应byte[]类型的地址的话就会有两个无法解决的问题:一是Java中的对象实际的内存布局跟C是不一样的,不同的JVM可能有不同的实现,byte[]的首地址可能只是个对象头,并不是真实的数据;二是垃圾收集器的存在使得JVM会经常移动对象的位置,这样同一个对象的真实内存地址随时都有可能发生变化,JVM知道地址变了,但是操作系统可不知道。
Netty中对零拷贝思想的第二处实现,就是在适当的位置直接使用堆外内存从而避免了数据从JVM Heap到C Heap的拷贝。
直接内存(Direct Memory)又叫做堆外内存,和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
元空间对应的内存也叫作直接内存,它们对应的都是机器的物理内存。
应用层的核心思想是减少数据在用户层的多次拷贝。比如在处理ByteBuffer中,我们有时候需要将多个ByteBuffer组合起来完成自己的业务逻辑,有时候我们需要开辟一个很大的Byte[]数组对他们进行处理。而Netty提供了CompositeByteBuf类,它提供了对多个ByteBuffer的一个"视图",可以将它们逻辑上当成一个完整的ByteBuffer来操作,这样就免去了重新分配空间再复制数据的开销。
CompositeByteBuf 在聚合时使用,多个buffer合并时,不需要copy,通过
CompositeByteBuf 可以把需要合并的bytebuf 组合起来,对外提供统一的readindex和writerindex
CompositeByteBuf 里面有个ComponentList,继承ArrayList,聚合的bytebuf都放在ComponentList里面,最小容量为16。
参考文章:
Netty对零拷贝(Zero Copy)三个层次的实现
理解Netty中的零拷贝(Zero-Copy)机制
【Netty】高性能原因:直接内存与零拷贝