- 1.1初始认识ByteBuf
- 1.1.1 ByteBuf的基础结构
- 1.1.2 ByteBuf的基本分类
- 1.2 ByteBufAllocator内存管理器
- 1.3 非池化内存分配
- 1.3.1 堆内内存的分配
- 1.4 池化内存分配
- 1.4.1 PooledByteBufAllocator简述
- 1.4.2 DirectArena内存分配流程
ByteBuf主要负责把数据从底层I/O读到ByteBuf,然后传递给应用程序,应用程序处理完成之后再把数据封装成ByteBuf写回I/O。
1.1.1 ByteBuf的基础结构我们来看netty
* +-------------------+------------------+------------------+ * | discardable bytes | readable bytes | writable bytes | * | | (CONTENT) | | * +-------------------+------------------+------------------+ * | | | | * 0 <= readerIndex <= writerIndex <= capacity
从上面ByteBuf的结构来看,我们发现ByteBuf有三个非常重要的指针,分别是readerIndex(记录读指针开始的位置)、writeIndex(记录写指针的开始位置)、和capacity(缓冲区的总长度)三者的关系是readerIndex <= writerIndex <= capacity。discardable bytes表示是无效的,readable bytes表示可读数据区,writable bytes表示这段数据空闲,可以往里面写数据。
1.1.2 ByteBuf的基本分类AbstractByteBuf有众多子类,大致可以分为三个维度进行分类,分别如下:
- Pooled:池化内存,就是从预先分配好的内存空间中提取一段连续内存封装成一个ByteBuf,分配给应用程序使用。
- Unsafe:是JDK底层的一个负责I/O操作对象,可以直接获得对象的内存地址,基于内存地址进行读写操作。
- Direct:堆外内存,直接调用JDK底层的API进行物理内存分配,不再JVM的堆内存中,需要手动释放。
Netty中内存分配有一个顶层的抽象就是ByteBufAllocator,负责分配所有ByteBuf类型的内存。
buffer()方法中对是否默认支持directBuffer判断,如果支持则分配directBuffer,否则分配heapBuffer。
我们发现newDirectBuffer()方法其实是一个抽象方法,最终,交给AbstractByteBufAllocator的子类实现。
分析到这里,已经知道directBuffer、heapBuffer、和Pooled、Unpooled的分配规则,那么Unsafe和非Unsafe是如何判别的呢?其实是Netty自动判别的,如果操作系统支持Unsafe那就使用Unsafe读写。
1.3 非池化内存分配 1.3.1 堆内内存的分配现在来看UnpooledByteBufAllocator的分配原理,首先是heapBuffer的分配逻辑,newHeapBuffer()方法代码如下:
UnpooledUnsafeHeapByteBuf和UnpooledHeapByteBuf都是调用的UnpooledHeapByteBuf的构造方法,那么它们之间到底有什么区别呢?其实根本区别在于I/O的读写,我们分别来看它们的getByte()方法,了解二者的区别。先看UnpooledHeapByteBuf的getByte()方法的实现代码。
就是根据index索引直接从数组中取值,接下来看UnpooledUnsafeHeapByteBuf的getByte()方法实现。
可以看到调用了Unsafe的getByte()方法,这是一个native()方法。它直接通过Buffer的内存地址加上一个偏移量去取数据。非Unsafe通过数组的下标取数据,Unsafe直接操作内存地址,相对于非Unsafe来说效率当然更高。
首先找到AbstractByteBufAllocator的子类PooledByteBufAllocator实现分配内存两个方法newHeapBuffer()和newDirectBuffer()
我们发现这两个方法大体机构都是一样的,以newDirectBuffer()方法为例,简单分析一下。
首先,通过threadCache.get()方法获得一个类型为PoolThreadCache的cache的对象;然后,通过cache获得directArena对象,最后,调用directArena.allocate方法分配ByteBuf。这里读者可能会有点看不懂,我们接下来详细分析一下,threadCache对象其实是PoolThreadLocalCache类型的变量,PoolThreadLocalCache相关代码如下。
首先调用leastUsedArena()方法分别获得类型为PoolArena的heapArena和direcArena对象,然后把heapArena和directArena对象作为参数传递到PoolThreadCache的构造器中,那么heapArena和directArena对象是哪里初始化的呢?经过查找,发现在PooledByteBufAlloctor的构造方法中调用newArenaArray()方法给heapArenas和directArenas进行了赋值。
其实就是创建了一个固定大小的PoolArena数组,数组大小由传入的参数nHeapArena和nHeapArena决定,通过构造方法向上找,可以找到两个这两个常量值。
它们的默认值都是CPU核数 * 2,EventLoopGroup分配线程时,默认线程数也是CPU核数*2,主要目的是就是保证Netty中的每一个任务线程都可以有一个独享的Arena,保证在每个线程分配内存的时候不用加锁。
基于上面分析,我们知道heapArena和directArena,这里统称为Arena。假设有四个线程,那么对应分配四个Arena。在创建ByteBuf的时候,首相通过PoolThreadCache获取Arena对象并且赋值给其成员变量,然后每个线程通过PoolThreadCache调用get()方法的时候会获得它底层Arena,也就是说EventLoop1获得Arena1,依次类推。
PoolThreadCache除了可以在Arena上进行内存分配,还可以在它底层维护的ByteBuf缓存列表进行分配。
1.4.2 DirectArena内存分配流程Arena分配内存的基本流程有三个步骤。
- 优先从对象池里获得PooledByteBuf进行复用
- 然后在缓存中进行内存分配
- 最后考虑从内存堆中进行内存分配
以directBuffer为例,首先来看从对象池中获得PooledByteBuf进行复用的情况,我们依旧跟进到PooledByteBufAllocator的newDirectBuffer()方法
首先调用newByteBuf()方法获得一个PooledByteBuf对象,然后通过allocate()方法在线程私有的PoolThreadCache中分配一块内存,再对buf里面的内存地址之类的进行初始化,跟进newByteBuf()方法,选择DirectArena对象。
首先判断是否支持Unsafe,默认情况下一般是支持Unsafe的,继续看PooledUnsafeDirectByteBuf的newInstance()方法,代码如下:
通过RECYCLER(内存回收站)对象的get()方法获得一个buf。从上面的代码片段看,RECYCLER对象实现了一个newObject方法,当回收站里面没有可用的buf时就会创建一个新的buf。因为获得buf可能是回收站取出来的,所以服用前需要重置。继续往下看就会调用buf的reuse()方法,代码如下。
reuse()方法就是让所有的参数重新归为初始状态。到这里我们应该已经清楚从内存池获取buf对象的全过程。接下来,再回到PoolArena的allocate方法,看看真实的内存是如何分配出来的?buf内存分配主要有两种情况,分别是从缓冲中进行内存分配和从内存堆里进行内存分配。
从对应的缓存中获取内存,如果所有规格都不满足,那就直接调用allocateHuge()方法进行真是的内存分配。



