ByteBuf是Netty整个结构⾥⾯最为底层的模块,主要负责把数据从底层I/O读到ByteBuf,然后传递给应⽤程序,应⽤程序处理完成之后再把数据封装成ByteBuf写回I/O。所以,ByteBuf是直接与底层打交道的⼀层抽象。相对于Netty其他模块来说,这部分内容是⾮常复杂的。
ByteBuf有三个⾮常重要的指针,分别是readerIndex(记录读指针的开始位置)、writerIndex(记录写指针的开始位置)和capacity(缓冲区的总长度),三者的关系是
readerIndex<=writerIndex<=capacity。从0到readerIndex为discardable bytes,表⽰是⽆效的;从readerIndex到writerIndex为readable bytes,表⽰可读数据区;从writerIndex到capacity为writablebytes,表⽰这段区间空闲,可以往⾥⾯写数据。除了这三个指针,ByteBuf⾥⾯其实还有⼀个指针maxCapacity,它相当于ByteBuf扩容的最⼤阈值。
在Netty中,ByteBuf的⼤部分功能是在AbstractByteBuf中实现,最重要的⼏个属性readerIndex、writerIndex、markedReaderIndex、markedWriterIndex、maxCapacity被定义在AbstractByteBuf抽象类中。
AbstractByteBuf有众多⼦类,⼤概类别分别如下。
● Pooled:池化内存,就是从预先分配好的内存空间中提取⼀段连续内存封装成⼀个ByteBuf,分给应⽤程序使⽤。● Unsafe:是JDK底层的⼀个负责I/O操作的对象,可以直接获得对象的内存地址,基于内存地址进⾏读写操作。● Direct:堆外内存,直接调⽤JDK的底层API进⾏物理内存分配,不在JVM的堆内存中,需要⼿动释放。Pooled(池化内存)和Unpooled(⾮池化内存);Unsafe和⾮Unsafe;Heap(堆内内存)和Direct(堆外内存)
Netty中内存分配有⼀个顶层的抽象就是ByteBufAllocator,负责分配所有ByteBuf类型的内存。ByteBufAllocator的基本实现类是AbstractByteBufAllocator,在newHeapBuffer()⽅法和newDirectBuffer()⽅法中,分配内存判断PlatformDependent是否⽀持Unsafe,如果⽀持则创建Unsafe类型的Buffer,否则创建⾮Unsafe类型的Buffer,由Netty⾃动判断。1.⾮池化内存分配1.1堆内内存分配
通过调⽤PlatformDependent.hasUnsafe()⽅法来判断操作系统是否⽀持Unsafe,如果⽀持Unsafe则创建UnpooledUnsafeHeapByteBuf类,否则创建UnpooledHeapByteBuf类。
1 public Class Unpooledheapbytebuf extends Abstractreferencecountedbytebuf f 2
3 private final Bytebufallocator alloc;bytel array 4
5 private Bytebuffer tmpniobuf; 6
7 protected Unpooledheapbytebuf( Bytebufallocator alloc, int initialcapacity, int maxcapacity) 8
9 this(alloc, new byte linitialcapacity], 0, 0, maxcapacity);10
11 protected Unpooledheapbytebuf(Bytebufallocator alloc, byte[] initialarray, int maxcapacity)12
13 this(alloc, initialarray, 0, initialarray. length, maxcapacity);14
15 private Unpooled Heapbytebuf(16
17 Bytebufallocator alloc, byte[] initialarray, int readerindex, int writerindex, int18
19 maxcapacity) t20
21 super(maxcapacity);22
23 this alloc= allocsetarray(initialarray);24
25 setindex(readerindex, writerindex);
其中调⽤了⼀个关键⽅法就是setArray()⽅法。这个⽅法的功能⾮常简单,就是把默认分配的数组new byte[initialCapacity]赋值给全局变量initialArray数组
Unsafe和不Unsafe根本区别在于I/O的读写,他们的getByte()⽅法,⾮Unsafe直接根据index索引从数组中取值,Unsafe则调⽤PlatformDependent⼯具类取值。
1.2堆外内存的分配
UnpooledByteBufAllocator的newDirectBuffer()⽅法实现堆外内存的分配,同样有Unsafe之分,Unsafe调⽤PlatformDependent.directBufferAddress()⽅法获取Buffer真实的内存地址,并保存到memoryAddress变量中,并调⽤了Unsafe的getLong()⽅法,这是⼀个native⽅法。它直接通过Buffer的内存地址加上⼀个偏移量去取数据。不管是堆外还是堆上,⾮Unsafe通过数组的下标取数据,Unsafe直接操作内存地址,相对于⾮Unsafe来说效率当然要更⾼。
2.池化内存分配
AbstractByteBufAllocator的⼦类PooledByteBufAllocator实现分配内存的两个⽅法:newDirectBuffer()⽅法和newHeapBuffer()⽅法。
以newDirectBuffer()⽅法为例,简单地分析⼀下。⾸先,通过threadCache.get()⽅法获得⼀个类型为PoolThreadCache的cache对象;然后,通过cache获得directArena对象;最后,调⽤directArena.allocate()⽅法分配ByteBuf。详细分析⼀下,threadCache对象其实是PoolThreadLocalCache类型的变量。从名字来看,PoolThreadLocalCache的
initialValue()⽅法就是⽤来初始化PoolThreadLocalCache的。⾸先调⽤leastUsedArena()⽅法分别获得类型为PoolArena的heapArena和directArena对象。然后把heapArena和directArena对象作为参数传递到PoolThreadCache的构造器中。那么heapArena和directArena对象是在哪⾥初始化的呢?经过查找,发现在PooledByteBufAllocator的构造⽅法中调⽤newArenaArray()⽅法给heapArenas和directArenas进⾏了赋值。
1 public Pooledbytebufallocator(boolean preferdirect, int nheaparena, int ndirectarena, intpagesize, int maxorder 2
3 int tinycachesize, int small Cachesize, int normalcachesize) { 4 if (nheaparena >0) { 5
6 heaparenas =newarenaarray(nheaparena); 7
8 } else { 9
10 heaparenas = null11
12 heaparenametrics =Collections. emptylist()13
14 if (ndirectarena > 0){15
16 directarenas =newarenaarray(ndirectarena);17 }18 }
newAreanArray()其实就是创建了⼀个固定⼤⼩的PoolArena数组,数组⼤⼩由传⼊的参数nHeapArena和nDirectArena决定
nHeapArena和nDirectArena的默认值就是CPU核数×2,也就是把defaultMinNumArena的值赋给nHeapArena和nDirectArena。对于CPU核数×2,EventLoopGroup分配线程时,默认线程数也是CPU核数×2。主要⽬的就是保证Netty中的每⼀个任务线程都可以有⼀个独享的Arena,保证在每个线程分配内存的时候不⽤加锁,实现了线程的绑定,基于上⾯的分析,我们知道Arena有heapArena和directArena,这⾥统称为Arena。假设有四个线程,那么对应会分配四个Arena。在创建ByteBuf的时候,⾸先通过
PoolThreadCache获取Arena对象并赋值给其成员变量,然后每个线程通过PoolThreadCache调⽤get()⽅法的时候会获得它底层的Arena,也就是说通过EventLoop1获得Arena1,通过EventLoop2获得Arena2,以此类推...
具体的池化思路就是
PoolThreadCache在Arena上进⾏内存分配,还可以在它底层维护的ByteBuf缓存列表进⾏分配。举个例⼦:我们通过PooledByteBufAllocator创建了⼀个1024字节的ByteBuf,当⽤完释放后,可能在其他地⽅会继续分配1024字节的ByteBuf。这时,其实不需要在Arena上进⾏内存分配,⽽是直接通过PoolThreadCache中维护的ByteBuf的缓存列表直接拿过来返回。在PooledByteBufAllocator中维护着三种规格⼤⼩的缓存列表,分别是三个值tinyCacheSize、smallCacheSize、normalCacheSize
/**附:这种思想在Tomcat⾥也有运⽤,⽐如Tomcat中的NioChannel属于频繁⽣成与消除的对象,因为每个客户端连接都需要⼀个通道与之相对应,频繁地⽣成和消除在性能的损耗上也不得不多加考虑,我们需要⼀种⼿段规避此处可能带来的性能问题。其思想就是:当某个客户端使⽤完NioChannel对象后,不对其进⾏回收,⽽是将它缓存起来,当新客户端访问到来时,只须替换其中的SocketChannel对象即可,NioChannel对象包含的其他属性只须做重置操作即可,如此⼀来就不必频繁⽣成与消除NioChannel对象。具体的做法是使⽤⼀个队列,⽐如ConcurrentLinkedQueue 在PooledByteBufAllocator的构造器中,分别赋值tinyCacheSize,smallCacheSize,normalCacheSize通过这种⽅式,Netty预创建了固定规格的内存池,⼤⼤提⾼了内存分配的性能。(Tomcat通重新为对象建⽴引⽤复⽤资源,Netty通过释放内存复⽤内存池⽽不是新开辟内存复⽤资源) Arena分配内存的基本流程有三个步骤。 (1)优先从对象池⾥获得PooledByteBuf进⾏复⽤。(2)然后在缓存中进⾏内存分配。 (3)最后考虑从内存堆⾥进⾏内存分配。 以directBuffer为例,⾸先来看从对象池⾥获得PooledByteBuf进⾏复⽤的情况,从PooledByteBufAllocator的newDirectBuffer()⽅法直接跟进PoolArena的allocate()⽅法,在该⽅法中调⽤newByteBuf()⽅法获得⼀个PooledByteBuf对象,然后通过allocate()⽅法在线程私有的PoolThreadCache中分配⼀块内存,再对buf⾥⾯的内存地址之类的值进⾏初始化。跟进newByteBuf()⽅法,选择DirectArena对象。然后通过RECYCLER(回收站对象)从不同规格的缓存列表获取对象。如果缓存列表的规格都不满⾜,就进⾏真实的内存分配。 其实在Netty底层还有⼀个内存单位的封装,为了更⾼效地管理内存,避免内存浪费,把每⼀个区间的内存规格⼜做了细分。默认情况下,Netty将内存规格划分为四个部分。Netty中所有的内存申请是以Chunk为单位向系统申请的,每个Chunk⼤⼩为16MB,后续的所有内存分配都是在这个Chunk⾥的操作。⼀个Chunk会以Page为单位进⾏切 分,8KB对应的是⼀个Page,⽽⼀个Chunk被划分为2048个Page。⼩于8KB的是SubPage。例如,我们申请的⼀段内存空间只有1KB,却给我们分配了⼀个Page,显然另外7KB就会被浪费,所以就继续把Page进⾏划分,以节省空间。内存规格⼤⼩如下图所⽰。 因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- sceh.cn 版权所有 湘ICP备2023017654号-4
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务