本文来分享Netty中的零拷贝机制以及内存缓冲区ByteBuf的实现。

源码分析基于Netty 4.1.52

Netty中的零拷贝

Netty中零拷贝机制主要有以下几种

1.文件传输类DefaultFileRegion#transferTo,调用FileChannel#transferTo,直接将文件缓冲区的数据发送到目标Channel,减少用户缓冲区的拷贝(通过linux的sendfile函数)。

使用read 和 write过程如下

使用sendfile

可以看到,使用sendfile函数可以减少数据拷贝以及用户态,内核态的切换

可参考: 操作系统和Web服务器那点事儿

2.Netty中提供了一些操作内存缓冲区的方法,如

Unpooled#wrappedBuffer方法,将byte数据,(jvm)ByteBuffer转换为ByteBuf

CompositeByteBuf#addComponents方法,合并ByteBuf

ByteBuf#slice方法,提取ByteBuf中部分数据片段

ByteBuf#duplicate,复制一个内存缓冲区

这些方法都是基于对象引用的操作,并没有内存拷贝,而是内存共享

3.使用堆外内存(jvm)ByteBuffer对Socket读写

如果使用JVM的堆内存读取Socket数据,JVM会将Socket数据读取到直接内存,再拷贝一份到堆内存中,写入数据到Socket也需要将堆内存拷贝一份到直接内存中,然后才写入Socket中。

因为操作系统进行io操作需要一个稳定的连续空间的字节空间, 但是java堆上的字节空间会随着gc进行而进行移动, 如果操作系统读取堆上的空间, 就会出错。

使用堆外内存可以避免该拷贝操作。

注意,这里从内核缓冲区拷贝到用户缓冲区的操作并不能省略,毕竟我们需要对数据进行操作,所以还是要拷贝到用户态的。

可参考:

知乎--Java NIO中,关于DirectBuffer,HeapBuffer的疑问

知乎--Java NIO direct buffer的优势在哪儿?

ByteBuf

ByteBuf是用于与Channel交互的内存缓冲区,提供顺序访问和随机访问。

Netty4中将ByteBuf调整为抽象类,从而提升吞吐量。

1.ByteBuffer

先了解一下ByteBuffer,ByteBuffer是JVM提供的字节内存缓冲区。ByteBuf是在ByteBuffer上进行的扩展,底层还是使用ByteBuffer。

ByteBuffer有两个子类,DirectByteBuffer和HeapByteBuffer。

HeapByteBuffer使用ByteBuffer#hb(byte[])存储数据。

DirectByteBuffer是堆外内存,使用的是操作系统的直接内存,它维护了一个引用address指向了底层数据,从而操作数据。(并没有使用ByteBuffer#buff)

Buffer核心属性

int position; //当前操作位置。
int mark; //为某一读过的位置做标记,便于某些时候回退到该位置。
int capacity; //初始化时候的容量。
int limit; // 读写的限制位置,读写超出该位置会报错

读写操作都是基于position,并以limit为限制的。mark,position,limit,capacity关系如下

0 <= mark <= position <= limit <= capacity

ByteBuffer提供了如下方法调整这些标志位置:

  • clear

    limit = position = 0

    一般在把数据写入Buffer前调用
  • flip

    limit = position

    position = 0

    一般在从Buffer读出数据前调用
  • rewind

    position=0

    limit不变

    一般在把数据重写入Buffer前调用。
  • compacting

    清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面

ByteBuffer还提供了一些操作缓冲区的方法

  • duplicate

    创建新字节缓冲区,共享当前缓冲区内容
  • slice

    创建新字节缓冲区,共享当前缓冲区内容子序列。

Netty的ByteBuf使用readerIndex标志读位置,writerIndex标志写位置,比(jvm)ByteBuffer设计更优雅。

  +-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
| | (CONTENT) | |
+-------------------+------------------+------------------+
| | | |
0 <= readerIndex <= writerIndex <= capacity

ByteBuf提供readerIndex/writerIndex等方法获取或设置这两个值,非常直观。另外,ByteBuf提供了如下方法操作缓冲区

  • discardReadBytes

    清除已经读过的数据。未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面

  • duplicate

    创建新字节缓冲区,共享当前缓冲区内容

  • slice(int index, int length)

    创建共享内存的ByteBuf,从index开始,长度为length

  • readSlice(int length)

    创建共享内存的ByteBuf,从readerIndex开始,长度为length

  • retainedDuplicate()

    创建共享内存的ByteBuf,并且当前ByteBuf的引用计数加1

2.接口关系

AbstractByteBuf:实现一些公共逻辑,如读写前检查位置。

AbstractReferenceCountedByteBuf,添加引用计数逻辑,实现引用计数回收直接内存。

PooledByteBuf:实现池化ByteBuf的公共逻辑。关于Netty中的内存池后面有文章解析。

PooledByteBuf#memory是底层的内存存储,PooledDirectByteBuf该字段是ByteBuffer,PooledHeapByteBuf则是byte[]。

下面可以分为Unsafe,No_Unsafe两个维度。Unsafe就是sun.misc.Unsafe。

使用Unsafe可以提高性能,但Unsafe是JDK内部的类,并非公开标准,不一定所有JDK都存在这个类, JDK以后也有可能去掉这个类,所以Netty提供了两套实现。

3.内存分配

后面有文章解析Netty内存池,分享Netty中如何分配内存给ByteBuf。这里先不深入。

4.读写过程

下面看一下ByteBuf与Channel如何交互数据。

前面分享Netty读写过程的文章说过了,NioByteUnsafe#read方法读取数据。

NioByteUnsafe#read -> NioSocketChannel#doReadBytes -> AbstractByteBuf#writeBytes -> PooledByteBuf#setBytes

public final int setBytes(int index, ScatteringByteChannel in, int length) throws IOException {
try {
return in.read(internalNioBuffer(index, length));
} catch (ClosedChannelException ignored) {
return -1;
}
}

index参数就是writerIndex,internalNioBuffer方法会构造一个新的ByteBuffer,并设置ByteBuffer#position为index

直接调用ReadableByteChannel#read读取数据

在《ChannelOutboundBuffer与flush操作》中已经分享过,

ChannelOutboundBuffer#nioBuffers也是通过internalNioBuffer方法生成ByteBuffer,

作为参数调用NioSocketChannel#doWrite方法,直接将数据拷贝到Channel。

ByteBuf#internalNioBuffer -> PooledByteBuf#_internalNioBuffer

  final ByteBuffer _internalNioBuffer(int index, int length, boolean duplicate) {
index = idx(index);
ByteBuffer buffer = duplicate ? newInternalNioBuffer(memory) : internalNioBuffer();
buffer.limit(index + length).position(index);
return buffer;
}

newInternalNioBuffer由子类实现,构建对应的DirectByteBuffer或者HeapByteBuffer,注意,这里的内存是共享的。

5.引用计数

由于使用了直接内存,不能依赖JVM垃圾回收器释放内存,Netty使用引用计数算法释放内存。

ReferenceCounted接口,代表需要显式释放的引用计数对象,retain方法增加引用计数,release方法减少引用计数。

AbstractReferenceCountedByteBuf实现了ReferenceCounted接口,它维护了refCnt变量作为引用计数。

构造一个AbstractReferenceCountedByteBuf时,refCnt为1。

当引用计数release到0时,调用deallocate()方法释放内存。

PooledByteBuf#deallocate

protected final void deallocate() {
if (handle >= 0) {
final long handle = this.handle;
this.handle = -1;
memory = null;
tmpNioBuf = null;
chunk.arena.free(chunk, handle, maxLength, cache);
chunk = null;
recycle();
}
}

这里调用的是PoolArena#free。

PoolArena可以理解为一个内存池,这里free实际是将内存放回内存池中,由内存池决定是否需要销毁底层直接内存。

PoolArena后面有对应文章解析。

6.内存销毁

销毁DirectByteBuf,有两个方式

利用反射获取Unsafe,调用Unsafe#freeMemory

利用反射获取DirectByteBuffer#cleaner(sun.misc.Cleaner),通过反射调用cleaner#clean方法

因为Netty不确认JDK中是否存在sun.misc.Cleaner,所以它也实现了两套机制。

PoolArenaDirect#free -> Arena#destroyChunk

protected void destroyChunk(PoolChunk<ByteBuffer> chunk) {
if (PlatformDependent.useDirectBufferNoCleaner()) {
PlatformDependent.freeDirectNoCleaner(chunk.memory);
} else {
PlatformDependent.freeDirectBuffer(chunk.memory);
}
}

从PlatformDependent中确认是否使用CLEANER

if (maxDirectMemory == 0 || !hasUnsafe() || !PlatformDependent0.hasDirectBufferNoCleanerConstructor()) {
USE_DIRECT_BUFFER_NO_CLEANER = false;
DIRECT_MEMORY_COUNTER = null;
}

满足以下条件中一个就使用CLEANER,否则使用NO_CLEANER

  1. 没有使用直接内存
  2. JVM不支持Unsafe
  3. ByteBuffer不存在无Cleaner的构造函数

如果您觉得本文不错,欢迎关注我的微信公众号。您的关注是我坚持的动力!

Netty源码解析 -- 零拷贝机制与ByteBuf的更多相关文章

  1. Netty源码解析 -- 事件循环机制实现原理

    本文主要分享Netty中事件循环机制的实现. 源码分析基于Netty 4.1 EventLoop 前面分享服务端和客户端启动过程的文章中说过,Netty通过事件循环机制(EventLoop)处理IO事 ...

  2. Netty源码解析—客户端启动

    Netty源码解析-客户端启动 Bootstrap示例 public final class EchoClient { static final boolean SSL = System.getPro ...

  3. Netty源码解析---服务端启动

    Netty源码解析---服务端启动 一个简单的服务端代码: public class SimpleServer { public static void main(String[] args) { N ...

  4. Netty 源码解析(三): Netty 的 Future 和 Promise

    今天是猿灯塔“365篇原创计划”第三篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel 当前:Ne ...

  5. Netty 源码解析(九): connect 过程和 bind 过程分析

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第九篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  6. Netty 源码解析(八): 回到 Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第八篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  7. Netty 源码解析(七): NioEventLoop 工作流程

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第七篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源 ...

  8. Netty 源码解析(六): Channel 的 register 操作

    原创申明:本文由公众号[猿灯塔]原创,转载请说明出处标注 今天是猿灯塔“365篇原创计划”第六篇. 接下来的时间灯塔君持续更新Netty系列一共九篇   Netty 源码解析(一 ):开始 Netty ...

  9. Netty 源码解析(五): Netty 的线程池分析

    今天是猿灯塔“365篇原创计划”第五篇. 接下来的时间灯塔君持续更新Netty系列一共九篇 Netty 源码解析(一): 开始 Netty 源码解析(二): Netty 的 Channel Netty ...

随机推荐

  1. Docker开启远程连接,本地IDEA使用docker插件连接(不认证的版本和认证的版本都有)

    前言 在学校学习的时候,要部署一个Java程序,一般是打成war包,放到服务器上的tomcat的webapp里面去: 后来SpringBoot出现内置了tomcat,就直接打成jar包,丢到服务器任何 ...

  2. JavaScript写秒表

    1.HTML部分 <div id="div1"> <span id="hour">00</span> <span> ...

  3. 强大的table组件-antd pro table

    概述 antd pro table antd pro table 的主要部分 表格显示的配置(绿色框内) 检索的配置(红色框内) 是否显示检索部分 检索的内容是如何生效的 工具栏的配置(黄色框内) 表 ...

  4. lua 1.0 源码分析 -- 总结

    读完 lua1.0 的源码感触:1. 把复杂的代码写简单2. pack 的内存回收3. hash 实现简单,但是应该可以改进,看高版本的代码怎么实现4. lua 初始化环境做了什么,就是一组全局变量初 ...

  5. MeteoInfoLab脚本示例:计算不同区域平均值

    这里用美国做例子,有一个美国区域的格点温度场数据(usgrid.data),需要计算出每个州(state)的平均温度.当然需要有一个包含各州行政区域的shape文件了(相关文件可以在此帖中下载:htt ...

  6. 一入Java深似海

    Java的基础语法 一个java程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来写协同工作.下面简要介绍下类.对象.方法和实例变量的概念. 对象: 对象是一个类的实例,有状态和行为.例如 ...

  7. spring-security-结合JWT的简单demo

    spring-security-demo 前言:本来是想尽量简单简单点的写一个demo的,但是spring-security实在是内容有点多,写着写着看起来就没那么简单了,想入门spring-secu ...

  8. linux学习(二)--setup.s

    执行过bootsect.s,加载了所有系统代码之后,开始向32位模式转变,为main函数的调用做准备,同样,附上图往下看 1 INITSEG = 0x9000 ! we move boot here ...

  9. Vue3 来了,Vue3 开源商城项目重构计划正式启动!

    我打算用 Vue3 写一个商城项目,目前已经开始着手开发,测试完成后正式开源到 GitHub,让大家也可以用现成的 Vue3 大型商城项目源码来练练手. Vue 3.0 来了,我们该做些什么? Vue ...

  10. css选择器 兄弟选择器 相邻兄弟选择器 子元素选择器

    1.兄弟选择器: ~ 该选择器会选择当前元素之后的所有相邻指定元素(具有相同父元素的兄弟元素): .p ~ li{ color: blue; } <div> <p class=&qu ...