您好,欢迎来到筏尚旅游网。
搜索
您的当前位置:首页netty源码分析之buffer

netty源码分析之buffer

来源:筏尚旅游网


NETTY的BYTEBUF源码分析

作者:淋雨

NETTY的BYTEBUF源码分析 ............................................................................................................ 1 一、 Java Buffer的相关基础知识 .................................................................................................. 3 1. 2. 3. 4. 5.

Java 基本数据类型 ..................................................................................................................... 3 Big-Endian和Little-Endian .......................................................................................................... 4 对象池技术 ................................................................................................................................. 6 对象引用 ..................................................................................................................................... 6 buddy allocation和slab allocation内存分配算法 ..................................................................... 7

二、 ByteBuf整体结构的分析 ...................................................................................................... 8 1.

ByteBuf整体结构 ........................................................................................................................ 9

三、 ByteBuf抽象层分析 .............................................................................................................. 9 1. 2. 3. 4.

ReferenceCounted 接口.............................................................................................................. 9 ByteBuf抽象类 .......................................................................................................................... 10 AbstractByteBuf类 .................................................................................................................... 13 AbstractReferenceCountedByteBuf 类 ..................................................................................... 17

四、 Buf的Unpooled的实现 ...................................................................................................... 18 1. 2. 3.

UnpooledHeapByteBuf .............................................................................................................. 19 UnpooledDirectByteBuf ............................................................................................................. 21 UnpooledUnsafeDirectByteBuf .................................................................................................. 22

五、 Pooled buffer的实现 ........................................................................................................... 22 1. 2. 3. 4. 5. 6.

Pooled Buffer整体结构 .......................................................................................................... 23 PoolChunk 的结构 .................................................................................................................. 24 PoolChunkList的结构 ............................................................................................................. 27 PoolArena的结构 .................................................................................................................... 28 从对象池中获取buffer ............................................................................................................ 29 Pooled Buffer内存监控 .......................................................................................................... 34

一、Java Buffer的相关基础知识 1. Java 基本数据类型 Java 中有8种基本类型:byte,char,short,int,long,float,double,boolean如下图: 原始类字节数 型 取值范围 byte 1 -128 ~ 127 boolean 1 0、1 short 2 -32768 ~ 32767 int 4 -2147483648 ~ 2147483647 包装类 Byte Boolean Short Integer 运算类型 JVM运算指令 int int int int iadd、isub、idiv、irem、ineg fadd、fsub、float 4 Float float fmul,fdiv,frem、fneg ladd、lsub、long 8 Long long lmul,ldiv,lrem、lneg dadd、dsub、double 8 Double double dmul,ddiv,drem、dneg char 2 持65536个字符 Character char JVM对于这8种基本类型的存储、访问、运算都直接在java栈内存中进行,在方法调用时存储在栈的本地局部变量中,运算完成后或者方法退出时可以立即清除。这样提供了快速、高效的访问、运算和回收的方式。

从上图中可以看出8种基本类型在存储的时候占用的是1-8个字节不等存储空间,假如给你这些字节的数组你该如何存储进入这些值呢,下面我来做一个简单演示。

通过上面的代码演示,我们通过位移的运算分别将short、int、long类型的值存储到2、4、8个字节中,每个字节存储的是数值在这个字节内的表示。

那对于float和double又该如何存储呢,JDK的API给我们提供了float的包装类Float提供了一个将float转换成int值的方法floatToRawIntBits,并提供了将转换后的int再转换成float逆向解析方法intBitsToFloat。这样我们就可以先将float转换成int进行存储了。double类型同理可以转换成long。

2. Big-Endian和Little-Endian  字节序定义 字节序,顾名思义字节的顺序,再多说两句就是大于一个字节类型的数据在内存中的存放顺序。在单个平台内部编程由于采用的是统一的编码排序,其实开发人员一般不用考虑这个问题,唯有在跨平台以及网络程序中才需要考虑这个问题。 在所有操作系统中,字节排序分总共为两类:。引用标准的Big-Endian和Little-Endian的定义如下:

a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

例子如下:

b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

例子如下:

c) 网络字节序:在网络传输中,4个字节的32 bit值以下面的次序传输:首先是0~7bit,

其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于 TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。这种只是按照字节的前后顺序去传输,并不涉及你用字符编码的顺序。  ByteOrder

在java 的ByteOrder类中定义BIG_ENDIAN和LITTLE_ENDIAN两个字段,来控制数值在字节缓冲区中排序的规则,  系统相关性 在/usr /include/中(包括子目录)查找字符串BYTE_ORDER(或_BYTE_ORDER, __BYTE_ORDER),确定其值。这个值一般在endian.h或machine/endian.h文件中可以找到,有时在feature.h中, 不同的操作系统可能有所不同。一般来说,Little Endian系统BYTE_ORDER(或_BYTE_ORDER,__BYTE_ORDER)为1234,Big Endian系统为4321。大部分用户的操作系统(如windows, Linux)是Little Endian的。少部分如MAC OS ,是Big Endian 的。本质上说,Little Endian还是Big Endian与操作系统和芯片类型都有关系。 下面是各种芯片操作系统对应的编码顺序参照表:

操作系统 x86系列(Intel, AMD, … ) DEC Alpha HP-PA NT HP-PA UNIX SUN SPARC All MIPS NT MIPS UNIX PowerPC NT PowerPC non-NT 编码排序方式 little-endian little-endian little-endian big-endian little-endian little-endian big-endian little-endian big-endian RS/6000 UNIX Motorola m68k big-endian big-endian

3. 对象池技术

在Java对象的生命周期大致包括三个阶段:对象的创建,对象的使用,对象的清除。

因此对象的生命周期长度可用如下的表达式表示:T = T1 + T2 +T3。其中T1表示对象的创建时间,T2表示对象的使用时间,而T3则表示其清除时间。其实能真正对我们产生作用的只有T2周期的时间,而T1、T3则是对象本身的开销。 整个对象的使用率:OP=T2/(T1+T2+T3) 对于有的对象创建周期T1、释放周期T3都比较大的情况下,例如数据库连接,网络连接等。我们的对象使用率就会很低,也就是意味着计算机的系统资源大部分都用来对象的创建和释放了。如下图:

对象池技术是将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象所造成的开销。用于充当保存对象的“容器”的对象,被称为“对象池”(Object Pool,或简称Pool)。 对于没有状态的对象(例如String),在重复使用之前,无需进行任何处理;对于有状态的对象(例如StringBuffer),在重复使用之前,就需要把它们恢复到等同于刚刚生成时的状态。由于条件的限制,恢复某个对象的状态的操作不可能实现了的话,就得把这个对象抛弃,改用新创建的实例了。

并非所有对象都适合放在对象池中,因为维护对象池也要造成一定开销。对生成时开销不大的对象进行池化,反而可能会出现“维护对象池的开销”大于“生成新对象的开销”,从而使性能降低的情况。对于有的对象创建周期T1、释放周期T3都比较大的情况下,对象池技术就是提高性能的有效策略了。

4. 对象引用

在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及(reachable)状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

 强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象。 String name=new String (“icy”);

 软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,GC不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要GC没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被GC回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

 弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在GC线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

 虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,他并不会对GC的执行策略产生任何影响。

虚引用主要用来跟踪对象被GC回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当GC准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。 常可以用它来检测对象池中的对象是否存在内存泄漏的问题,例如某个对象直接从对象池中取出对象,我们可以将它维护一个虚引用记录下来,如果这个对象被回收了,没有对象池的信息更新的话就可以通过这个监控来得到,是否存在泄漏。

5. buddy allocation和slab allocation内存分配算法  buddy allocation

buddy算法是用来做内存管理的经典算法,目的是为了解决内存的外碎片。buddy算法将所有空闲页框分组为11个块链表,每个块链表的每个块元素分别包含

1,2,4,8,16,32,64,128,256,512,1024个连续的页框,每个块的第一个页框的物理地址是该块大小的整数倍。如,大小为16个页框的块,其起始地址是16*2^12(一个页框的大小为4k,16个页框的大小为16*4K,1k=1024=2的10次方,4k=2的12次方)的倍数。

例,假设要请求一个128个页框的块,算法先检查128个页框的链表是否有空闲块,如果没有则查256个页框的链表,有则将256个页框的块分裂两份,一份使用,一份插入128个页框的链表。如果还没有,就查512个页框的链表,有的话就分裂为128,128,256,一个128使用,剩余两个插入对应链表。如果在512还没查到,则返回出错信号。

回收过程相反,内核试图把大小为b的空闲伙伴合并为一个大小为2b的单独快,满足以下条件的两个块称为伙伴:1,两个块具有相同的大小,记做b;2,它们的物理地址是连续的,3,第一个块的第一个页框的物理地址是2*b*2^12的倍数,该算法迭代,如果成功合并所释放的块,会试图合并2b的块来形成更大的块。

 slab allocation

Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法。Jeff 的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其他常见结构)分配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。因此他的结论是不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始化函数(mutex_init)即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和析构函数之后,它已经处于所需的状态中了。

Linux slab 分配器使用了这种思想和其他一些思想来构建一个在空间和时间上都具有高效性的内存分配器。

图 1 给出了 slab 结构的高层组织结构。在最高层是 cache_chain,这是一个 slab 缓存的链接列表。这对于 best-fit 算法非常有用,可以用来查找最适合所需要的分配大小的缓存(遍历列表)。cache_chain 的每个元素都是一个 kmem_cache 结构的引用(称为一个 cache)。它定义了一个要管理的给定大小的对象池。

二、ByteBuf整体结构的分析

ByteBuf提供了多种方式的buffer实现,按照内存分配的方式可以分为heap和direct两种实现方式。按照buffer的重用性可以分为Unpooled和Pooled两种实现方式。

从上面两种维度的组合,提供了多种实现方式的buffer,具体如下: 重用性 内存 非对象池 对象池 Heap内存 Direct内存 UnpooledHeapByteBuf UnpooledDirectByteBuf UnpooledUnsafeDirectByteBuf PooledHeapByteBuf PooledDirectByteBuf PooledUnsafeDirectByteBuf 1. ByteBuf整体结构

三、ByteBuf抽象层分析

1. ReferenceCounted 接口

在整个UML中最上层的接口是ReferenceCounted,这个接口从名称上我们就知道他是用来做引用计数的。当一个对象被使用的时候我们就+1,当我们一个对象不用的时候就-1.就是一个对象的引用计数器,当对象引用计数为0的时候就表示当前对象不再有使用,可以分配

给其他线程。这也是对象池计数的一个标准实现方式。

接口中的retain()和retain(int increment)方法使用标记当前对象被使用几次的。release()和release(int decrement)方法是用来释放引用计数的。在对象从对象池中取出会调用retain()方法打上该对象已经使用,当对象不在使用的时候会调用release()方法释放对象,以供其他线程使用。refCnt()方法是用来获取该对象是否在使用的标志。 2. ByteBuf抽象类

ByteBuf 是整个NETTY的buffer的总接口,其中主要定义了缓冲区读写、容量控制,数据的刷新与清除等功能。完全实现了JDK自带的ByteBuffer全部功能,并根据NETTY自身的系统架构的需要,扩充了对InputStream和ScatteringByteChannel直接读写等。 我们可以将ByteBuf接口定义的功能进行以下的归类:  提供了对8种基本类型的读写功能

 定义了对buffer的当前读写状态的判断,主要用来判断当前的buffer是否可以读写的功

能。

 对整个buffer容量的设置和获取

 对buffer的存储数据是采用什么编码方式的设定

 定义了对缓冲区是heap的还是direct,并定义获取内存地址的方法: public abstract boolean isDirect();

public abstract boolean hasMemoryAddress(); public abstract long memoryAddress();

3. AbstractByteBuf类

AbstractByteBuf作为ByteBuf的直接的抽象类,主要完成了ByteBuf接口定义的对buffer的当前读写状态的判断,主要用来判断当前的buffer是否可以读写的功能。 在AbstractByteBuf中设置了readerIndex(当前读的位置),writerIndex(当前写入的位置),markedReaderIndex(备份的读的位置),markedWriterIndex(备份写入的位置),maxCapacity(系统最大容量)等五个字段,并通过五个字段的位置来判断当前buffer可读、可写入的状态。

下面给出一些方法的源码;

1 对readerIndex和writerIndex的获取和设定

2 对当前buffer的可读、可写状态的判断

3 确保buffer写入的支持方法,确保buffer在容量不够的时候也能自动扩容以达到对写入的支持。

Buffer的扩容算法,这个对于整个buffer的动态扩充的算法

4 对8种基本数据类型的读、写操作给了初步实现。为什么说是初步实现呢,是因为他提供的是初步的包装,最终读取字节是由具体的各种buffer子类实现的。 对8种类型的读取操作:

对8种类型的数据写入操作:

4. AbstractReferenceCountedByteBuf 类

AbstractReferenceCountedByteBuf 是个抽象类,是ReferenceCounted接口的直接实现。主要完成了对buffer对象引用计数的统计。在AbstractReferenceCountedByteBuf中定义了refCnt对象应用计算字段,该字段是refCnt保证了多线程的可见性。同时定义了AtomicIntegerFieldUpdater类型的变量refCntUpdater,通过CAS原理,可以确保多线程修改的正确性。

实现了类对对象使用的引用计数,引用计数为默认值1,进行引用计数+1。当对象释放的时候将引用计数-1。 当引用计数为默认值1的时候,就说明没有对象使用进行对象释放。 Retain 方法:

Release方法:

四、Buf的Unpooled的实现

Buf的Unpooled直接实现,主要有heap和direct两种实现方式。Heap是用字节数组的方式实现,主要实现类UnpooledHeapByteBuf。Direct使用内存的方式实现,主要实现类有UnpooledDirectByteBuf和UnpooledUnsafeDirectByteBuf。

1. UnpooledHeapByteBuf

UnpooledHeapByteBuf定义字节数组array来存储的数据信息的,并定义了Buf 向 ByteBuffer转换的临时变量tmpNioBuf。并完成了数据读、写的最终实现。

数组扩容的方法capacity

数据具体读取方法的实现:

数据具体写入方法的时下:

ByteBuffer转换功能:

2. UnpooledDirectByteBuf

UnpooledDirectByteBuf 类是将JDK自带的ByteBuffer的direct buffer做了一层封装。实现的功能和UnpooledHeapByteBuf基本一致,只是在具体的实现方式上有所差别。

数据的读取是通过ByteBuffer的读取方法来间接实现的:

数据的写入是通过ByteBuffer的写入方法来间接实现的:

3. UnpooledUnsafeDirectByteBuf

UnpooledUnsafeDirectByteBuf是通过unsafe来操作数据的读写,功能和

UnpooledDirectByteBuf是相似的。当系统支持unsafe的时候,默认direct buffer 就是UnpooledUnsafeDirectByteBuf的。

五、Pooled buffer的实现

Netty 4.x开发了Pooled Buffer,采用对象池技术实现了一个高性能的buffer池,分配策略则是结合了buddy allocation和slab allocation的jemalloc变种。在网络编程中提供了高效的buffer创建方式,同时减少了buffer的创建和垃圾回收的消耗。

1. Pooled Buffer整体结构

Pooled Buffer 主要由PoolArena来规划一大片的buffer缓冲区(字节数组、direct内存区域)。PoolArena又由一系列的PoolChunkList组成双向指针链表,来将具体的每个PoolChunk单元(相当操作系统内存的page)合并成一个大的buffer缓冲区。并将PoolChunk划分为大小相同的多个PoolSubpage,以方便进行具体buffer分配。 整体的逻辑结构图如下:

类之间的引用关系如下:

2. PoolChunk 的结构

PoolChunk是对象池中进行分配的主要管理单元,他管理了多个内存页(PoolSubpage)。 每次从PoolChunk申请buffer的时候会subpages数组中获取到一个未分配的内存页,然后进行buffer的分配。

在PoolChunk创建的时候,同时构造了Memory Map(表示节点状态信息),Depth Map(用来表示每个节点的初始状态),Subpages(存储内存页数组)。

Subpages为2N字方的数组,Memory Map为2(N+1)字方的数组,其实是一个二叉树的数组方式实现。Depth Map为二叉树的初始化状态值。Memory Map只有叶子节点才表示存储信息,非叶子节点只是用来表示叶子节点的状态信息。没次取节点的时候通过深度遍历边节点开始取

 一次性取一页的逻辑 第一次取分页后数组状态:

第二次取分页后数组状态:

第四次取分页之后状态

 一次性取两页的逻辑

3. PoolChunkList的结构

PoolChunkList通过双向指针链表将多个节点连接起来,并对每个节点里面存储的Chunk能使用的使用率做出限制。对超出使用率限制的Chunk,会向上一个节点或者下一个节点迁移。这样就能起到优先从使用率较低的chunk中分配内存页,以减少内存页分配时二叉树遍历的次数。

当超出使用率限制的Chunk进行位移

移动后的结果

4. PoolArena的结构

PoolArena主要是包含了Pool Chunk List、Tiny Subpage Pools和Small Subpage Pools这三个组成部分。Pool Chunk List维护了整个Chunk的链表组合结构。Tiny Subpage Pools和Small Subpage Pools存储了从Chunk中分配出来的PoolSubpage。

5. 从对象池中获取buffer

allocator1 初始化2 获取bufferPoolArenaPoolThreadCachePooledByteBufPooledChunkPoolSubpage3 从cache查找4 构造个PooledByteBuf5 创建chunk6 创建PoolSubpage7分配PoolSubpage8将分配的PoolSubpage放到缓存中9 初始化PooledByteBuf10 返回对象引用 1 初始化PoolArena

主要是构造PoolChunkList的双向指针链表,够一个整体的Chunk List。同时初始化了tiny page pool 和small page pool,这两个pool是Pooled Buffer主要缓存实现。Tiny 是指小于512个字节的buffer,small 是指大于512 并且小于page size的buffer。

2 从PoolArena中获取buffer

先从当前线程的缓存中查找到对应PoolArena,然后从PoolArena中获取buffer。根据需要申请buffer大小来判断获取buffer的方式。具体的流程图如下:

获取buffer小于512 根据size来确定获取方式大于512、小于page size大于page size从tiny page pool获取分页查询记录从small page pool获取获取buffer没有是否获取到buffer有创建新的buffer初始化buffer结束 具体的代码如下:

3 构造新的chunk

当查找从cache中查找无法得到buffer的时候,创建一个新的chunk来扩充缓存的大小。 并在新的chunk中划分出PooledByteBuf。

newChunk 方法在PoolArena是一个抽象的方法,最终的实现时由他的子类HeapArena和DirectArena来实现的。最终调用的是PoolChunk构造方法。

在PoolChunk 的构造方法中对整个二叉树的结构进行了初始化。并初始化了

PoolSubpage的数组对象。也就是在这个构造方法中真正实现了buffer的定义。

5 从chunk中分配Subpage对象

根据需要申请buffer的大小判断需要申请的是一个或者多个Subpage所表示的buffer大小。

获取一个空闲页的大小buffer实现方法如下:

从根节点开始 逐层向下查找各个节点获取到 空闲的节点算法实现如下:

7 分配PoolSubpage

因为请求的容量不足一个subpage的容量,会对PoolSubpage按照请求的大小进行等分,分成N个小格子,以充分使用内存大小。同时将当前的PoolSubpage加入到PoolArena 缓存中,以方便使用。

9 PooledByteBuf的初始化

主要是获取到PooledByteBuf对象在整个对象缓存区的中的开始位置和结束位置。

6. Pooled Buffer内存监控  引用计数

NETTY中使用引用计数机制来管理资源,当一个实现ReferenceCounted的对象实例化时,引用计数置1. 客户代码中需要保持一个该对象的引用时需要调用接口的retain方法将计数增1.对象使用完毕时调用release将计数减1. 当引用计数变为0时,对象将释放所持有的底层资源或将资源返回资源池.  内存泄露

按上述规则使用Direct和Pooled的ByteBuf时,内存使用情况的监控非常重要。DirectBuf,其内存不受VM垃圾回收控制只有在调用release导致计数为0时才会主动释放内存,而PooledByteBuf只有在release后才能被回收到池中,以便以循环利用。如果用户只使用不去release,将会导致内存泄露。最终会导致整个系统OOM。  内存泄露检测

NETTY通过java 的虚引用(PhantomReference)来实现了内存使用情况的跟踪。在从Pool中取出buffer会被包装成一个虚引用的对象,并通过门面模式封装成SimpleLeakAwareByteBuf、AdvancedLeakAwareByteBuf对象来对外提供功能。 具体实现模型如下:

通过判断ReferenceQueue中在垃圾回收的对象是否还是左边链表中的节点就可以确定是否存在内存泄露。因为在调用release方法时候会将ResourceLeak从链表中删除。

1通过门面模式服装检测资源

2体获取到检测资源的方法

3 具体构造虚引用的对象的实现

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- efsc.cn 版权所有

违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务