I/O 加速:与众不同的 Netty 零拷贝技术
零拷贝是一个耳熟能详的词语,在 Linux、Kafka、RocketMQ 等知名的产品中都有使用,通常用于提升 I/O 性能。而且零拷贝也是面试过程中的高频问题,那么你知道零拷贝体现在哪些地方吗?Netty 的零拷贝技术又是如何实现的呢?
为什么要有DMA技术?
在没有 DMA 技术前,I/O 的过程是这样的:
- CPU 发出对应的指令给磁盘控制器,然后返回;
- 磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断;
- CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。
整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。
简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来。
计算机科学家们发现了事情的严重性后,于是就发明了 DMA 技术,也就是直接内存访问(Direct Memory Access) 技术。
什么是 DMA 技术?
简单理解就是,在磁盘设备和内存进行数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务。
那使用 DMA 控制器进行数据传输的过程究竟是什么样的呢?下面我们来具体看看。
具体过程:
- 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态;
- 操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务;
- DMA 进一步将 I/O 请求发送给磁盘;
- 磁盘收到 DMA 的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA 发起中断信号,告知自己缓冲区已满;
- DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务;
- 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU;
- CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回;
可以看到, 整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。
早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。
传统的文件传输:4切4拷
如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
代码通常如下,一般会需要两个系统调用:
java
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
代码很简单,虽然就两行代码,但是这里面发生了不少的事情。
初学 Java 时,我们在学习 IO 和 网络编程时,会使用以下代码:
java
File file = new File("index.html");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] arr = new byte[(int) file.length()];
raf.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
我们会调用 read 方法读取 index.html 的内容------ 变成字节数组,然后调用 write 方法,将 index.html 字节流写到 socket 中,那么,我们调用这两个方法,在 OS 底层发生了什么呢?我这里借鉴了一张其他文字的图片,尝试解释这个过程。
md
先上一张图,这张图就代表了传统 IO 传输文件的流程。
读取文件的时候,会从用户态切换为内核态,同时基于 DMA 引擎将磁盘文件拷贝到内核缓冲区。
看到这里,可能你就已经懵逼了,什么是用户态和内核态,什么是 DMA 拷贝,我用大白话解释一下
首先用户态其实就是 CPU 在执行你的代码,而内核态呢,其实就是你没有那个权限去操作硬件,所以只能交给系统去调用,这个时候就是内核态。
举个例子,你的女朋友需要你修个电脑(醒醒,但凡有一粒花生米也不至于喝成这样),我换个说法,假如你同班的女同学想让你修个电脑,但是宿管阿姨不肯放你进女生宿舍,这个时候你就是用户态,你不能进女生宿舍,所以你只能让宿管阿姨(内核态)来帮你把电脑取出来。
那什么是 DMA 拷贝呢,DMA(DirectMemoryAccess,直接内存存取)其实就是因为 CPU 老哥太累了,所以找了个小弟,就是 DMA 替他完成一部分的拷贝工作,这样 CPU 就能去做其他事情了。
md
讲完了内核态和用户态还有 DMA 的大概意思,我们接着回到刚才的 IO 流程中。
1.read 调用导致用户态到内核态的一次变化,同时,第一次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU,拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU)引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。
简而言之:磁盘--->内核缓存区
总结: 1次拷贝 1次切换(用户态到内核态)
2.发生第二次数据拷贝,即:将内核缓冲区的数据拷贝到用户缓冲区,同时,发生了一次用内核态到用户态的上下文切换。
简而言之:内核缓存区--->用户缓存区(可以理解为java进程)
总结: 1次拷贝 1次切换(内核态到用户态)
3.发生第三次数据拷贝,我们调用 write 方法,系统将用户缓冲区的数据拷贝到 Socket 缓冲区。此时,又发生了一次用户态到内核态的上下文切换。
简而言之:用户缓存区(可以理解为java进程)--->内核缓存区(Socket 缓冲区)
总结: 1次拷贝 1次切换
4.第四次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
简而言之:内核缓存区(Socket 缓冲区)--->网络协议引擎
总结: 1次拷贝
5.write 方法返回,再次从内核态切换到用户态。
总结: 1次切换
那么,这里指的用户态、内核态指的是什么?上下文切换又是什么?
简单来说,用户空间指的就是用户进程的运行空间,内核空间就是内核的运行空间。
如果进程运行在内核空间就是内核态,运行在用户空间就是用户态。
为了安全起见,他们之间是互相隔离的,而在用户态和内核态之间的上下文切换也是比较耗时的。
从上面我们可以看到,一次简单的IO过程产生了4次上下文切换,这个无疑在高并发场景下会对性能产生较大的影响。
那么什么又是 DMA 拷贝呢?
因为对于一个 IO 操作而言,都是通过 CPU 发出对应的指令来完成。但是,相比 CPU 来说,IO 的速度太慢了,CPU 有大量的时间处于等待 IO 的状态。
因此就产生了 DMA(Direct Memory Access)直接内存访问技术,本质上来说他就是一块主板上独立的芯片,通过它来进行内存和 IO 设备的数据传输,从而减少 CPU 的等待时间。
但是无论谁来拷贝,频繁的拷贝耗时也是对性能的影响。
通过上面的步骤可以发现传统的 IO 操作执行,有 4 次上下文的切换和 4 次拷贝,是不是很繁琐。
md
我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。
这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。
所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
传统的数据拷贝过程为什么不是将数据直接传输到用户缓冲区呢?其实引入内核缓冲区可以充当缓存的作用,这样就可以实现文件数据的预读,提升 I/O 的性能。
但是当请求数据量大于内核缓冲区大小时,在完成一次数据的读取到发送可能要经历数倍次数的数据拷贝,这就造成严重的性能损耗。
接下来我们介绍下使用零拷贝技术之后数据传输的流程。重新回顾一遍传统数据拷贝的过程,可以发现第二次和第三次拷贝是可以去除的,DMA 引擎从文件读取数据后放入到内核缓冲区,然后可以直接从内核缓冲区传输到 Socket 缓冲区,从而减少内存拷贝的次数。
在 Linux 中系统调用 sendfile() 可以实现将数据从一个文件描述符传输到另一个文件描述符,从而实现了零拷贝技术。
☆mmap映射优化:4切3拷
mmap + write
在前面我们知道了,传统的文件传输方式会历经 4 次数据拷贝,而且这里面,「从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到 socket 的缓冲区里」,这个过程是没有必要的。
因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」 ,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的。
在前面我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。
mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
mmap+write 简单来说就是使用 mmap 替换了 read+write 中的 read 操作,减少了一次 CPU 的拷贝。
mmap 主要实现方式是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,从而减少了从读缓冲区到用户缓冲区的一次CPU拷贝。
mmap 通过内存映射,将文件映射到用户进程缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图:
md
大致过程如下:
应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;
应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。
整个过程发生了 4 次用户态和内核态的上下文切换和 3 次拷贝,具体流程如下:
1.mmap 调用导致用户态到内核态的一次变化,同时,第1次复制开始:DMA(Direct Memory Access,直接内存存取,即不使用 CPU,拷贝数据到内存,而是 DMA 引擎传输数据到内存,用于解放 CPU)引擎从磁盘读取 index.html 文件,并将数据放入到内核缓冲区。
DMA控制器把数据从硬盘中拷贝到内核缓冲区,上下文从内核态转为用户态,mmap 调用返回。
简而言之:磁盘--->内核缓存区 同时进行了内核缓冲区的数据映射到了用户缓冲区。
总结:1次拷贝 2次切换
2.我们调用 write 方法,系统将内核缓冲区的数据拷贝到 Socket 缓冲区,发生第2次数据拷贝。
此时,又发生了一次用户态到内核态的上下文切换。
总结:1次拷贝 1次切换
3.第3次拷贝,数据异步的从 Socket 缓冲区,使用 DMA 引擎拷贝到网络协议引擎。这一段,不需要进行上下文切换。
简而言之:内核缓存区(Socket 缓冲区)--->网络协议引擎
总结:1次拷贝
4.write 方法返回,再次从内核态切换到用户态。
总结: 1次切换
md
传统:磁盘-内核 内核-用户 用户-内核(socket缓存区) 内核(socket缓存区) -协议引擎(网卡)
mmap:磁盘-内核 内核-内核(socket缓存区) 内核(socket缓存区) -协议引擎(网卡)
mmap 通过内存映射,将文件从内核缓冲区映射到用户缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
传统 IO 里面从内核缓冲区到用户缓冲区有一次 CPU 拷贝,从用户缓冲区到 Socket 缓冲区又有一次 CPU 拷贝。mmap 则一步到位,直接基于 CPU 将内核缓冲区的数据拷贝到了 Socket 缓冲区。
mmap 的方式节省了一次 CPU 拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。
之所以能够减少一次拷贝,就是因为 mmap 直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于 DMA 拷贝的,同时用户缓冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了。
我们可以得知,通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。
但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。
RocketMQ 中就是使用的 mmap 来提升磁盘文件的读写性能。
如你所见,3次拷贝,4次切换,拷贝切换操作太多了。如何优化这些流程?
sendfile系统调用函数:linux2.1:2切3拷
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:
cmd
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。
其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:
相比 mmap 来说,sendfile 同样减少了一次 CPU 拷贝,而且还减少了 2 次上下文切换。
java
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
sendfile 是 Linux2.1 内核版本后引入的一个系统调用函数。通过使用 sendfile 数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间的拷贝 ,同时由于使用 sendfile 替代了 read+write 从而节省了一次系统调用,也就是 2 次上下文切换。
md
整个过程发生了 2 次用户态和内核态的上下文切换和 3 次拷贝,具体流程如下:
用户进程通过 sendfile() 方法向操作系统发起调用,上下文从用户态转向内核态;
总结:1次切换
DMA控制器把数据从硬盘中拷贝到内核缓冲区。
总结:1次拷贝
CPU将内核缓冲区中数据拷贝到 socket 缓冲区。注意这里是cpu拷贝
总结:1次拷贝
DMA 控制器把数据从 socket 缓冲区拷贝到网卡,上下文从内核态切换回用户态,sendfile 调用返回。
总结:1次拷贝
sendfile 方法 IO 数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。
总结:mmap减少了一次拷贝 sendFile减少了1次拷贝 2次切换
但是这还不是真正的零拷贝技术,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。
你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:
cmd
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
sendfile系统调用函数:linux2.4:2切2拷
Linux2.4 内核版本之后对 sendfile 做了进一步优化,通过引入新的硬件支持,这个方式叫做 DMA Scatter/Gather 分散/收集功能。
- 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
- 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;
所以,这个过程之中,只进行了 2 次数据拷贝,如下图:
它将读缓冲区中的数据描述信息------内存地址和偏移量记录到 socket 缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次 CPU 拷贝的过程。
md
整个过程发生了 2 次用户态和内核态的上下文切换和 2 次拷贝,其中更重要的是完全没有 CPU 拷贝,具体流程如下:
用户进程通过 sendfile() 方法向操作系统发起调用,上下文从用户态转向内核态;
DMA 控制器利用 scatter 把数据从硬盘中拷贝到内核缓冲区离散存储;
CPU 把内核缓冲区中的文件描述符和数据长度发送到 socket 缓冲区;注意是发送!
DMA 控制器根据文件描述符和数据长度,使用 scatter/gather 把数据从内核缓冲区拷贝到网卡;
sendfile() 调用返回,上下文从内核态切换回用户态。
DMA gather和 sendfile 一样数据对用户空间不可见,而且需要硬件支持,同时输入文件描述符只能是文件,但是过程中完全没有CPU拷贝过程,极大提升了性能。
DMA 引擎读取文件内容并拷贝到内核缓冲区,然后并没有再拷贝到 Socket 缓冲区,只是将数据的长度以及位置信息被追加到 Socket 缓冲区,然后 DMA 引擎根据这些描述信息,直接从内核缓冲区读取数据并传输到协议引擎中,从而消除最后一次 CPU 拷贝。
md
可以看到在图中,已经没有了用户缓冲区,因为用户缓冲区是在用户空间的,所以没有了用户缓冲区也就意味着不需要上下文切换了,就省略了这一步的从内核态切换为用户态。
同时也不需要基于 CPU 将内核缓冲区的数据拷贝到 Socket 缓冲区了,只需要从内核缓冲区拷贝一些 offset 和 length 到 Socket 缓冲区。
接着从内核态切换到用户态,从内核缓冲区直接把数据拷贝到网络协议引擎里去;同时从 Socket 缓冲区里拷贝一些 offset 和 length 到网络协议引擎里去,但是这个 offset 和 length 的量很少,几乎可以忽略。
sendFile 整个过程只有两次上下文切换和两次 DMA 拷贝,很重要的一点是这里完全不需要 CPU 来进行拷贝了,所以才叫做零拷贝,这里的拷贝指的就是操作系统的层面。
那你肯定会问,那 mmap 里面有一次 CPU 拷贝为啥也算零拷贝,只能说那不算是严格意义上的零拷贝,但是他确实是优化了普通 IO 的执行流程,就像老婆饼里也没有老婆嘛。
Kafka 和 Tomcat 内部使用就是 sendFile 这种零拷贝。
RocketMQ 选择了 mmap + write 这种零拷贝方式,适用于业务级消息这种小块文件的数据持久化和传输;而 Kafka 采用的是 sendfile 这种零拷贝方式,适用于系统日志消息这种高吞吐量的大块文件的数据持久化和传输。但是值得注意的一点是,Kafka 的索引文件使用的是 mmap + write 方式,数据文件使用的是 sendfile 方式。
mmap 和 sendFile 的区别
- mmap 适合小数据量读写,sendFile 适合大文件传输。
- mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 2 次上下文切换,最少 2 次数据拷贝。
- sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
在这个选择上:rocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。
小结
由于 CPU 和 IO 速度的差异问题,产生了 DMA 技术,通过 DMA 搬运来减少 CPU 的等待时间。
传统的 IOread+write 方式会产生 2 次 DMA 拷贝 + 2 次 CPU 拷贝,同时有 4 次上下文切换。
而通过 mmap+write 方式则产生 2 次 DMA 拷贝 + 1 次 CPU 拷贝,4 次上下文切换,通过内存映射减少了一次 CPU 拷贝,可以减少内存使用,适合大文件的传输。
sendfile 方式是新增的一个系统调用函数,产生 2 次 DMA 拷贝 + 1 次 CPU 拷贝,但是只有 2 次上下文切换。因为只有一次调用,减少了上下文的切换,但是用户空间对 IO 数据不可见,适用于静态文件服务器。
sendfile+DMA gather 方式产生 2 次 DMA 拷贝,没有 CPU 拷贝,而且也只有2次上下文切换。虽然极大地提升了性能,但是需要依赖新的硬件设备支持。
传统 IO 执行的话需要 4 次上下文切换(用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态)和 4 次拷贝(磁盘文件 DMA 拷贝到内核缓冲区,内核缓冲区 CPU 拷贝到用户缓冲区,用户缓冲区 CPU 拷贝到 Socket 缓冲区,Socket 缓冲区 DMA 拷贝到协议引擎)。
mmap 将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上,适合小数据量读写,需要 4 次上下文切换(用户态 -> 内核态 -> 用户态 -> 内核态 -> 用户态)和3 次拷贝(磁盘文件DMA拷贝到内核缓冲区,内核缓冲区 CPU 拷贝到 Socket 缓冲区,Socket 缓冲区 DMA 拷贝到协议引擎)。
sendfile 是将读到内核空间的数据,转到 socket buffer,进行网络发送,适合大文件传输,只需要 2 次上下文切换(用户态 -> 内核态 -> 用户态)和 2 次拷贝(磁盘文件 DMA 拷贝到内核缓冲区,内核缓冲区 DMA 拷贝到协议引擎)
Jdk零拷贝
在 Java 中也使用了零拷贝技术,它就是 NIO FileChannel 类中的 transferTo() 方法,transferTo() 底层就依赖了操作系统零拷贝的机制,它可以将数据从 FileChannel 直接传输到另外一个 Channel。基于Java 层操作优化,对数组缓存对象(ByteBuf )进行封装优化,通过对ByteBuf数据建立数据视图,支持ByteBuf 对象合并,切分,当底层仅保留一份数据存储,减少不必要拷贝
案例-osChannel-transferFrom()
从目标通道中去复制原通道数据
java
@Test
public void test02() throws Exception {
// 1、字节输入管道
FileInputStream is = new FileInputStream("data01.txt");
FileChannel isChannel = is.getChannel();
// 2、字节输出流管道
FileOutputStream fos = new FileOutputStream("data03.txt");
FileChannel osChannel = fos.getChannel();
// 3、复制
osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size());
isChannel.close();
osChannel.close();
}
案例-isChannel-transferTo()
把原通道数据复制到目标通道
java
@Test
public void test02() throws Exception {
// 1、字节输入管道
FileInputStream is = new FileInputStream("data01.txt");
FileChannel isChannel = is.getChannel();
// 2、字节输出流管道
FileOutputStream fos = new FileOutputStream("data04.txt");
FileChannel osChannel = fos.getChannel();
// 3、复制
isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
isChannel.close();
osChannel.close();
}
Netty零拷贝
使用逻辑组合,代替实际复制。
CompositeByteBuf实际上是一个虚拟化的ByteBuf,作为一个ByteBuf特殊的子类,可以用来对多个ByteBuf统一操作,一般情况下,CompositeByteBuf对多个ByteBuf操作并不会出现复制拷贝操作,只是保存原来ByteBuf的引用。
CompositeByteBuf有1个属性:private Component[] components;
什么是Component?
java
private static final class Component {
final ByteBuf buf;
int adjustment;
int offset;
int endOffset;
}
原来就是把多个ByteBuf包装成Component然后统一放到数组里。
例如 CompositeByteBuf: io.netty.handler.codec.ByteToMessageDecoder#COMPOSITE_CUMULATOR
使用包装,代替实际复制。
java
byte[] bytes = data.getBytes();
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);
php
public static ByteBuf wrappedBuffer(byte[] array) {
if (array.length == 0) {
return EMPTY_BUFFER;
}
return new UnpooledHeapByteBuf(ALLOC, array, array.length);
}
调用 JDK 的 Zero-Copy 接口。
Netty 中也通过在 DefaultFileRegion 中包装了 NIO 的 FileChannel.transferTo() 方法实现了零拷贝:io.netty.channel.DefaultFileRegion#transferTo
堆外内存
md
堆外内存生活场景:
夏日,小区周边的烧烤店铺,人满为患坐不下,店家常常怎么办? 解决思路:店铺门口摆很多桌子招待客人。
•店内 -> JVM 内部 -> 堆(heap) + 非堆(non heap)
•店外 -> JVM 外部 -> 堆外(off heap)
优点:
•更广阔的"空间 ",缓解店铺内压力 -> 破除堆空间限制,减轻 GC 压力
•减少"冗余"细节(假设烧烤过程为了气氛在室外进行:烤好直接上桌:vs 烤好还要进店内)-> 避免复制
缺点:
•需要搬桌子 -> 创建速度稍慢
•受城管管、风险大 -> 堆外内存受操作系统管理