Netty 高性能之零拷贝
更多内容,前往个人博客
零拷贝是指避免在用户态(User-space)与内核态(Kernel-space)之间来回拷贝数据的技术。
一、传统 IO
传统 IO(InputStream/OutputStream)读取数据并通过网络发送数据的流程,如下图:

【1】当用户空间使用 read() 询问内核空间时,会进行一次上下文的切换(从用户态切换到内核态)。然后内核空间通过 sys_read() 或等价的方法(与操作系统相关)从磁盘中读取数据(ask for data)。使用 DMA(Direct Memory Access,直接内存存取)引擎执行第一次拷贝:从磁盘空间拷贝数据到内核缓冲区中。
【2】请求的数据从内核读缓冲区拷贝到用户缓冲区(cope data to user buffer),这里又进行了一次上下文切换(从内核态切换到用户态)现在待读取的数据已存储在用户空间内存的缓冲区。至此,完成了一次 IO 的读取过程。
【3】wirte() 调用将用户空间数据拷贝到内核缓冲区(与上面的不同,这次指的是 socket 缓冲区) 中,也会进行一次上下文切换(从用户态切换到内核态)。然后再由内核缓冲区将数据 writes data 到目标文件。
【4】最终 write returns(调用返回)会进行第四次上下文切换(从内核态切换到用户态)。
磁盘到内核空间属于 DMA 拷贝,用户空间与内核空间之间的数据传输并没有类似 DMA 这种可以不需要 CPU 参与的传输方式,因此用户空间与内核空间之间的数据传输是需要 CPU 全程参于的。
DMA(Direct Memory Access,直接内存存取),原理是外部设备不需要通过 CPU 就可以直接与系统内存交换数据;所以也就有了零拷贝技术,避免不必要的 CPU 数据拷贝过程。
二、NIO 零拷贝(方案一)
NIO 的零拷贝由 transferTo 方法实现。transferTo 方法将数据从 FileChannel 对象传送到可写的字节通道(如Socket Channel等)。在 transferTo 方法内部实现中,由 native 方法 transferTo0 来实现,它依赖操作系统底层的支持。在 UNIX 和 Linux 系统中,调用这个方法会引起 sendfile() 系统调用,实现了数据直接从内核的读缓冲区传输到套接字缓冲区,避免了用户态(User-space)与内核态(Kernel-space)之间的数据拷贝。
客户端发起 sendfile() 后,进行上下文切换到内核状态。内核向磁盘发起 ask for data,使用的是操作系统的 transferTo 方法调用触发 DMA 引擎将文件上下文信息拷贝到内核缓冲区中。与上面的不同是,此时,不会再将数据发送给用户缓冲区(减少复制和上下文切换)。而是 write data to target socket buffer(将数据从内核缓冲区拷贝到目标 socket(套接字) 相关联的缓冲区)
相比于传统 IO ,使用 NIO 零拷贝后。我们将上下文切换次数从4次减少到了2次。将数据拷贝次数从4次减少到3次(其中一次涉及了 CPU,另外2次是 DMA 直接存取)
1 /**
2 * filechannel进行文件复制(零拷贝)
3 *
4 * @param fromFile 源文件
5 * @param toFile 目标文件
6 */
7 public static void fileCopyWithFileChannel(File fromFile, File toFile) {
8 try (// 得到fileInputStream的文件通道
9 FileChannel fileChannelInput = new FileInputStream(fromFile).getChannel();
10 // 得到fileOutputStream的文件通道
11 FileChannel fileChannelOutput = new FileOutputStream(toFile).getChannel()) {
12
13 //将fileChannelInput通道的数据,写入到fileChannelOutput通道
14 fileChannelInput.transferTo(0, fileChannelInput.size(), fileChannelOutput);
15 } catch (IOException e) {
16 e.printStackTrace();
17 }
18 }
三、NIO 零拷贝(方案二)
如果操作系统底层支持 gather 操作,可以进一步减少内核中的数据拷贝,在 Linux2.4 以及更高版本的内核中,socket 缓冲区描述符已被修改用来适应这个需求。这种方式不但减少了上下文切换,同时消除了 CPU 参于的重复的数据拷贝。
用户使用方式不变,依旧通过 TransferTo 方法,但是方法的内部实现发生了变化:
【1】transferTo 方法调用触发 DMA 引擎将文件上下文信息拷贝到内核缓冲区。
【2】数据不会拷贝到套接字缓冲区,只是数据的描述符(包括数据位置和长度)被拷贝到套接字缓冲区。DMA 引擎直接将数据从内核缓冲区拷贝到 Hardware,这样就减少了最后一次需要消耗 CPU 的拷贝操作。
四、NIO 零拷贝适用场景
【1】文件较大,读写较慢,追求速度;
【2】JVM 内存不足,不能加载太大数据;
【3】内存带宽不够,即存在其它程序或线程存在大量的 IO 操作,导致带宽减小。
五、直接内存映射
在不需要进行数据文件操作时,可以使用NIO的零拷贝。但如果既需要IO速度,又需要进行数据操作,则需要使用NIO的直接内存映射。
Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间;同样地, 内核空间对这段区域的修改也直接反映用户空间。正因为有这样的映射关系, 就不需要在用户态(User-space) 与内核态 (Kernel-space) 之间拷贝数据, 提高了数据传输的效率,这就是内存直接映射技术。
【1】NIO的直接内存映射:JDK1.4 加入了 NIO 机制和直接内存,目的是防止 Java堆和 Native堆之间数据复制带来的性能损耗,此后 NIO可以使用 Native的方式直接在 Native堆分配内存。

1 //背景:堆内数据在flush到远程时,会先复制到Native 堆,然后再发送;直接移到堆外就更快了。
2 static int write(FileDescriptor paramFileDescriptor,ByteBuffer paramByteBuffer, long paramLong, NativeDispatcher paramNativeDispatcher) throws IOException{
3 //如果是直接缓冲区,则不用复制,直接写出
4 if((paramByteBuffer instanceof DirectBuffer)){
5 Return writeFromNativeBuffer(paramFileDescriptor, paramByteBuffer, paramLong, paramNativeDispatcher);
6 }
7 //如果不是直接缓冲区,会对缓冲数据进行赋值
8 int i = paramByteBuffer.position();
9 int j = paramByteBuffer.limit();
10 assert (i <= j);
11 int k = i <= j ? j-i : 0;
12 //赋值的数据放入 localByteBuffer
13 ByteBuffer localByteBuffer = Util.getTemporaryDirectBuffer(k);
14 try{
15 localByteBuffer.put(paramByteBuffer);
16 localByteBuffer.flip();
17 paramByteBuffer.position(i);
18 int m = writeFromNativeBuffer(paramFileDescriptor,localByteBuffer,paramLong,paramNativeDispatcher);
19 if(m > 0){
20 paramByteBuffer.position(i + m);
21 }
22 return m;
23 } finally {
24 Util.offerFirstTemporaryDirectBuffer(localByteBuffer);
25 }
26 }
【2】直接内存的创建:在 ByteBuffer 有两个子类,HeapByteBuffer 和 DirectByteBuffer。前者是存在于 JVM堆中的,后者是存在于 Native堆中的。

【3】申请堆内存与直接内存的代码如下:
1 //申请堆内存
2 public static ByteBuffer allocate(int capacity) {
3 if (capacity < 0)
4 throw new IllegalArgumentException();
5 return new HeapByteBuffer(capacity, capacity);
6 }
7
8 //申请直接内存
9 public static ByteBuffer allocateDirect(int capacity) {
10 return new DirectByteBuffer(capacity);
11 }
六、使用直接内存的原因
【1】对垃圾回收停顿的改善。因为full gc时,垃圾收集器会对所有分配的堆内内存进行扫描,垃圾收集对 Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响 Java应用的性能。如果使用堆外内存的话,堆外内存是直接受操作系统管理。这样做的结果就是能保持一个较小的 JVM堆内存,以减少垃圾收集对应用的影响。(full gc时会触发堆外空闲内存的回收)
【2】减少了数据从 JVM拷贝到 native堆的次数,在某些场景下可以提升程序 I/O的性能。
【3】以突破 JVM内存限制,操作更多的物理内存。当直接内存不足时会触发full gc,排查full gc的时候,一定要考虑。
七、使用直接内存的问题
【1】堆外内存难以控制,如果内存泄漏,那么很难排查(VisualVM 可以通过安装插件来监控堆外内存)。
【2】外内存只能通过序列化和反序列化来存储,保存对象速度比堆内存慢,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。
【3】直接内存的访问速度(读写方面)会快于堆内存。在申请内存空间时,堆内存速度高于直接内存。
直接内存适合申请次数少,访问频繁的场合。如果内存空间需要频繁申请,则不适合直接内存。
八、NIO 的直接内存映射
NIO 中一个重要的类:MappedByteBuffer nio引入的文件内存映射方案,读写性能极高。MappedByteBuffer 将文件直接映射到内存。可以映射整个文件,如果文件比较大的话可以考虑分段进行映射,只要指定文件的感兴趣部分就可以。
由于 MappedByteBuffer 申请的是直接内存,因此不受 Minor GC 控制,只能在发生 Full GC 时才能被回收,因此 Java提供了DirectByteBuffer 类来改善这一情况。它是 MappedByteBuffer 类的子类,同时它实现了 DirectBuffer 接口,维护一个 Cleaner对象来完成内存回收。因此它既可以通过 Full GC来回收内存,也可以调用 clean()方法来进行回收。
九、NIO 的直接内存映射的函数调用
FileChannel 提供了 map方法来把文件映射为内存对象:
MappedByteBuffer map(int mode,long position,long size);
可以把文件的从 position 开始的 size大小的区域映射为内存对象,mode 指出了可访问该内存映像文件的方式:
■ READ_ONLY(只读): 试图修改得到的缓冲区将导致抛出 ReadOnlyBufferException.(MapMode.READ_ONLY)
■ READ_WRITE(读/写): 对得到的缓冲区的更改最终将传播到文件;该更改对映射到同一文件的其他程序不一定是可见的。 (MapMode.READ_WRITE)
■ PRIVATE(专用): 对得到的缓冲区的更改不会传播到文件,并且该更改对映射到同一文件的其他程序也不是可见的;相反,会创建缓冲区已修改部分的专用副本。 (MapMode.PRIVATE)
使用参数 -XX:MaxDirectMemorySize=10M,可以指定 DirectByteBuffer的大小最多是10M。
直接内存映射代码演示 :
1 static final int BUFFER_SIZE = 1024;
2
3 /**
4 * 使用直接内存映射读取文件
5 * @param file
6 */
7 public static void fileReadWithMmap(File file) {
8
9 long begin = System.currentTimeMillis();
10 byte[] b = new byte[BUFFER_SIZE];
11 int len = (int) file.length();
12 MappedByteBuffer buff;
13 try (FileChannel channel = new FileInputStream(file).getChannel()) {
14 // 将文件所有字节映射到内存中。返回MappedByteBuffer
15 buff = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
16 for (int offset = 0; offset < len; offset += BUFFER_SIZE) {
17 if (len - offset > BUFFER_SIZE) {
18 buff.get(b);
19 } else {
20 buff.get(new byte[len - offset]);
21 }
22 }
23 } catch (IOException e) {
24 e.printStackTrace();
25 }
26 long end = System.currentTimeMillis();
27 System.out.println("time is:" + (end - begin));
28 }
29
30 /**
31 * HeapByteBuffer读取文件
32 * @param file
33 */
34 public static void fileReadWithByteBuffer(File file) {
35
36 long begin = System.currentTimeMillis();
37 try(FileChannel channel = new FileInputStream(file).getChannel();) {
38 // 申请HeapByteBuffer
39 ByteBuffer buff = ByteBuffer.allocate(BUFFER_SIZE);
40 while (channel.read(buff) != -1) {
41 buff.flip();
42 buff.clear();
43 }
44 } catch (IOException e) {
45 e.printStackTrace();
46 }
47 long end = System.currentTimeMillis();
48 System.out.println("time is:" + (end - begin));
49 }
Netty 高性能之零拷贝的更多相关文章
- 理解Netty中的零拷贝(Zero-Copy)机制【转】
理解零拷贝 零拷贝是Netty的重要特性之一,而究竟什么是零拷贝呢? WIKI中对其有如下定义: “Zero-copy” describes computer operations in which ...
- 对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解
此文章已同步发布在我的 segmentfault 专栏. 根据 Wiki 对 Zero-copy 的定义: "Zero-copy" describes computer opera ...
- netty如何实现零拷贝
根据 Wiki 对 Zero-copy 的定义: "Zero-copy" describes computer operations in which the CPU does n ...
- Netty:Netty中的零拷贝(Zero Copy)
零复制概念: " 零复制"描述了计算机操作,其中CPU不执行将数据从一个存储区复制到另一个存储区的任务.通过网络传输文件时,通常用于节省CPU周期和内存带宽. WIKI的定义中,我 ...
- NIO学习笔记,从Linux IO演化模型到Netty—— Java NIO零拷贝
同样只是大致上的认识. 其中,当使用transferFrom,transferTo的时候用的sendfile(). 如果系统内核不支持 sendfile,进一步执行 transferToTrusted ...
- 感悟优化——Netty对JDK缓冲区的内存池零拷贝改造
NIO中缓冲区是数据传输的基础,JDK通过ByteBuffer实现,Netty框架中并未采用JDK原生的ByteBuffer,而是构造了ByteBuf. ByteBuf对ByteBuffer做了大量的 ...
- NIO学习笔记,从Linux IO演化模型到Netty—— Netty零拷贝
Netty的中零拷贝与上述零拷贝是不一样的,它并不是系统层面上的零拷贝,只是相对于ByteBuf而言的,更多的是偏向于数据操作优化这样的概念. Netty中的零拷贝: 1.CompositeByteB ...
- Netty源码解析 -- 零拷贝机制与ByteBuf
本文来分享Netty中的零拷贝机制以及内存缓冲区ByteBuf的实现. 源码分析基于Netty 4.1.52 Netty中的零拷贝 Netty中零拷贝机制主要有以下几种 1.文件传输类DefaultF ...
- Linux、JDK、Netty中的NIO与零拷贝
一.先理解内核空间与用户空间 Linux 按照特权等级,把进程的运行空间分为内核空间和用户空间,分别对应着下图中, CPU 特权等级分为4个,Linux 使用 Ring 0 和 Ring 3. 内核空 ...
- Netty高性能网络应用框架对标P7面试题分享v4.1.70.Final
概述 **本人博客网站 **IT小神 www.itxiaoshen.com 定义 Netty官网 https://netty.io/ 最新版本为4.1.70.Final Netty是一个异步的.事件驱 ...
随机推荐
- Vue的学习(1)
在学习Vue之前,首先大家要知道一个mvvm模式,何为mvvm模式呢,mvvm其实是有m,v和vm组成,类似与java里面的mvc模式,只不过mvc模式是针对于后台来说,而mvvm是针对于前台来说的, ...
- Parallels Desktop 18(Mac虚拟机)v18.0.0(53049)无限试用版+win11系统
Parallels Desktop 18 for Mac 是一款强大的虚拟机软件,让您无需重启即可在 Mac 上运行 Windows 应用程序不会减慢 Mac 的运行速度,具有速度快.操作简单且功能强 ...
- 黑马 java.lang.IllegalArgumentException: Property ‘dataSource‘ is required
现象: 按照教程步骤做的,但连单元测试都无法通过,会出现java.lang.IllegalArgumentException: Property 'dataSource' is required这个错 ...
- matlab解析毫米波雷达bin文件数据 得到复数
来源:TI提供的Mmwave Radar Device ADC Raw Data Capture %%% This script is used to read the binary file pro ...
- Hyperkernel验证实验的复现与z3环境初识
Hyperkernel验证实验的复现与z3环境初识 前言 建议顺序阅读本文!禁止转载.--@CarpVexing(https://www.cnblogs.com/CarpVexing/p/159483 ...
- GreenPlum tidb 性能比较
主要的需求 针对大体量表的OLAP统计查询,需要找到一个稳定,高性能的大数据数据库,具体使用 数据可以实时的写入和查询,并发的tps不是很高 建立数据仓库,模式上主要采用星星模型.雪花模型,或者宽表 ...
- 3.javaweb-servlet
1. 设置编码 tomcat8之前,设置编码: 1)get请求方式: //get方式目前不需要设置编码(基于tomcat8) //如果是get请求发送的中文数据,转码稍微有点麻烦(tomcat8之前) ...
- 西电oj245 成绩统计 结构体数组使用
#include<stdio.h> struct student{ //定义一个结构体数组 int num; char name[11]; float g1; float g2; floa ...
- MFS分布式存储特性及组件说明
1.MFS MooseFS是一个具有冗余容错功能的分布式网络文件系统,它将数据分别存放在多个物理服务器或单独磁盘或分区上,确保一份数据有多个备份副本,然而对于访问MFS的客户端或者用户来说,整个分布式 ...
- Python第七章实验报告
一.实验名称:<零基础学Python>第7章 面向对象程序设计 二.实验环境:IDLE Shell 3.9.7 三.实验内容:5道实例.4道实战 四.实验过程: 实例01 创建大雁类并定义 ...