Netty源码解析

Stella981
• 阅读 756

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

Netty中的零拷贝

Netty中零拷贝机制主要有以下几种
1.文件传输类DefaultFileRegion#transferTo,调用FileChannel#transferTo,直接将文件缓冲区的数据发送到目标Channel,减少用户缓冲区的拷贝(通过linux的sendfile函数)。
使用read 和 write过程如下
Netty源码解析

使用sendfile
Netty源码解析

可以看到,使用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.接口关系

Netty源码解析

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源码解析

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
Netty之缓冲区ByteBuf解读(一)
!(https://oscimg.oschina.net/oscnet/up6de4d71f462d9846befe00ec6505125a928.JPEG)\Netty在数据传输过程中,会使用缓冲区设计来提高传输效率。虽然,Java在NIO编程中已提供ByteBuffer类进行使用,但是在使用过程中,其编码方式相对来说不太友好,也
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这