netty源码浅读 - 内存管理


netty内存管理架构借鉴的是JEMalloc分配算法。

ByteBuf

ByteBuf作为netty内存管理对象的抽象,统一了内存管理相应的api。如:
read*()、write*()、get*()、set*(),以及markReaderIndex()、markWriterIndex()、resetReaderIndex(),resetWriterIndex()。

ByteBuf数据结构

在这里插入图片描述
简言之,一般情况下,ByteBuf提供两个指针对数据进行读写,直观上从左到右依次是读指针,写指针;即0—readerIndex之间是已读数据(可丢弃的数据),readerIndex—writerIndex之间是可读数据,writerIndex—capacity之间是可写数据(实际上是writerIndex到maxCapacity之间)。
这样看来,read*()方法会向右移动readerIndex,具体移动几个字节,要看read何种数据类型,例如调用readByte(),readerIndex会右移一个字节,并且读取该字节的内容并返回;
同理,write*()方法会向右移动writerIndex;
get*(),set*()会直接针对内存数组索引进行操作;
markWriterIndex(),resetWriterIndex()操作的是markedWriterIndex指针,调用markWriterIndex()会将当前writerIndex赋值给markedWriterIndex,调用resetWriterIndex()会将writerIndex的值还原为markedWriterIndex,这个两个方法提供了我们多次写入同一段内存的能力。
同样,markReaderIndex(),resetReaderIndex()会涉及到一个特殊的指针markedReaderIndex,这两个方法提供了我们多次读取同一段内存的能力。

ByteBuf继承体系

在这里插入图片描述
ByteBuf有一个默认骨架实现,即AbstractByteBuf,实现了大部分的功能,留有具体_get*()、_set*()方法提供给子类实现不同的读取存入内存方式。
由AbstractByteBuf派生的具体内存管理的类分为三类:

  1. 池化和非池化(Pooled和UnPooled)
  • 带有Pooled前缀的均为池化内存持有对象,即:分配内存时,会从内存池中取出适合的一块内存。
  • 带有UnPooled前缀的均为非池化内存持有对象。
  1. Unsafe和非Unsafe
  • 内存管理类名称带有Unsafe的,持有的内存是使用jdk底层unsafe分配的内存
  1. 直接内存和堆内存(Direct和heap)
  • Direct修饰的内存管理类,作为内存持有对象时,可以认为持有的内存是堆外内存,使用时要注意内存泄漏问题,如使用完要对内存进行释放;反之,Heap~持有的内存是受虚拟机控制的,也就是可以被GC。

整个ByteBuf体系是建立在这三个维度上的,比如:PooledUnsafeDirectByteBuf是池化的,Unsafe的以及Direct的结合。

ByteBufAllocator

ByteBufAllocator:内存分配器。
ByteBuf只是持有资源,对外提供操作资源的接口,并不关心资源从哪里来,以及自身何时被初始化等问题,那么这个时候ByteBufAllocator就闪亮登场啦,ByteBufAllocator负责提供(具体提供方式根据子类的区别而略显不同)具体内存资源以及对ByteBuf进行初始化。

ByteBufAllocator具体职能

在这里插入图片描述

  1. buffer()/buffer(int initialCapacity)/buffer(int initialCapacity, int maxCapacity):分配一个ByteBuf,具体是direct还是heap的依赖具体的实现
  2. ioBuffer()/ioBuffer(int initialCapacity)、ioBuffer(int initialCapacity, int maxCapacity):分配一个适合IO的ByteBuf(direct)
  3. heapBuffer()/~/ :分配一个堆内的ByteBuf
  4. directBuffer()/~/:分配一个持有直接内存的ByteBuf

PS:initialCapacity表示初始化 容量,maxCapacity表示最大可用容量

ByteBufAllocator继承体系

在这里插入图片描述
ByteBufAllocator的抽象实现AbstractByteBufAllocator将ByteBufAllocator相应的api全部实现,仅仅提供了newHeapBuffer(int initialCapacity, int maxCapacity)&newDirectBuffer(int initialCapacity, int maxCapacity)供不同的子类去实现。

  1. PooledByteBufAllocator:如名,池化内存分配器
  2. UnpooledByteBufAllocator:非池化内存分配器

PoolArena

PoolArena是池化内存的管理者,负责组织协调池化内存,ByteBufAllocator分配池化内存底层依赖PoolArena(ByteBufAllocator组合了PoolArena)。

PoolArena数据结构

PoolArena除了维护一些基本的内存分配相关的数据之外,还维护了六个PoolChunkList,以及两个PoolSubpage。
数据结构如下图:
在这里插入图片描述
类中表示为:
在这里插入图片描述
PoolChunkList之前构成双向链表(qInit除外),并且每个PoolChunkList内部的PoolChunk之间也是双向链表的结构。在实际分配内存活动中,PoolChunk根据内部使用率的不同会在q000~q100链表之间移动,这样做更有利于内存的分配以及减少内存碎片化。

PoolArena继承体系

在这里插入图片描述
主要实现为HeapArena和DirectArena

netty内存可视化

PoolChunk

PoolChunk是netty向操作系统申请内存的基本单位,默认情况下PoolChunk持有16M内存。并且PoolChunk中会维护很多相关的属性:

  • 所属的PoolArena
  • 是否池化内存
  • 偏移地址(offset)
  • 所属的PoolChunkList
  • 与当前chunk构成链表的前驱和后继
  • PoolSubpage数组 — 子页数组,分配更小的内存
  • memoryMap — 存放使用完全二叉树表示内存的分配信息,且在分配过程中一般是以page(8K)为单位。
  • depthMap — 存放节点在二叉树中的深度信息

PoolSubpage

在向netty申请内存过程中,如果要申请的内存小于8K,则会将Chunk中的叶子节点(page)继续拆分为SubPage,从而适应小内存的分配。
同样的PoolSubpage内部维护了自身相关的一些属性:

  • 所属PoolChunk
  • elemSize — 按照elemSize切分page,即每块subPage的内存大小
  • 与当前PoolSubpage构成链表的前驱和后继

netty内部PoolSubpage内存规格根据elemSize的大小可以分为Tiny&Small,分配内存少于512B则为tiny规格,内存大于512B少于8k的为small规格。

具体分配内存流程分析

前面谈到的是内存管理涉及到的点,要真正的理解netty的内存管理还需要将各个点连接起来去感受。

分析入口

public class NettyRAM {


    public static void main(String[] args) {
        PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT;
        ByteBuf buffer = allocator.buffer(1024*8);
    }
}

以池化内存分配器举例说明,使用PooledByteBufAllocator.DEFAULT来得到一个PooledByteBufAllocator。
在这里插入图片描述

具体分析

  1. 走到PooledByteBufAllocator的构造方法,如下:

      public PooledByteBufAllocator(boolean preferDirect, int nHeapArena, int nDirectArena, int pageSize, int maxOrder,
                                  int tinyCacheSize, int smallCacheSize, int normalCacheSize,
                                  boolean useCacheForAllThreads, int directMemoryCacheAlignment) {
      // 省略一堆我觉得不太重要的代码
        if (nHeapArena > 0) {
            heapArenas = newArenaArray(nHeapArena);
            List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(heapArenas.length);
            for (int i = 0; i < heapArenas.length; i ++) {
                PoolArena.HeapArena arena = new PoolArena.HeapArena(this,
                        pageSize, maxOrder, pageShifts, chunkSize,
                        directMemoryCacheAlignment);
                heapArenas[i] = arena;
                metrics.add(arena);
            }
            heapArenaMetrics = Collections.unmodifiableList(metrics);
        } else {
            heapArenas = null;
            heapArenaMetrics = Collections.emptyList();
        }
    
        if (nDirectArena > 0) {
            directArenas = newArenaArray(nDirectArena);
            List<PoolArenaMetric> metrics = new ArrayList<PoolArenaMetric>(directArenas.length);
            for (int i = 0; i < directArenas.length; i ++) {
                PoolArena.DirectArena arena = new PoolArena.DirectArena(
                        this, pageSize, maxOrder, pageShifts, chunkSize, directMemoryCacheAlignment);
                directArenas[i] = arena;
                metrics.add(arena);
            }
            directArenaMetrics = Collections.unmodifiableList(metrics);
        } else {
            directArenas = null;
            directArenaMetrics = Collections.emptyList();
        }
        metric = new PooledByteBufAllocatorMetric(this);
    }
    

    通过PooledByteBufAllocator的构造方法可以看出,在构造PooledByteBufAllocator的时候,会做一些必要的事情:

    • 实例化threadCache,threadCache是一个PoolThreadLocalCache对象,PoolThreadLocalCache是netty内部FastThreadLocal的子类,类似于jdk的ThreadLocal;
    • 初始化PooledByteBufAllocator内部组合的PoolArena数组(heapArenas&heapArenas,默认8个)

    以初始化directArenas为例分析

  2. 初始化PoolArena
    在初始化PoolArena的时,会初始化PoolChunkList,并将PoolChunkList按指定顺序排好。在这里插入图片描述
    刚刚初始化的PoolArena是不持有实际内存的,即六个PoolChunkList均没有PoolChunk,到此,PooledByteBufAllocator初始化完成。

  3. 执行分配内存逻辑

    	 ByteBuf buffer = allocator.buffer(1028*8);
    
  • 先经过AbstractByteBufAllocator处理
    在这里插入图片描述
    在这里插入图片描述
    • 具体构造ByteBuf通过AbstractByteBufAllocator的抽象方法newDirectBuffer延迟到子类(本例的PooledByteBufAllocator)去实现,无处不在的模板方法模式~
      在这里插入图片描述
      从threadCache中获取到directArena(PoolArena$directArena),通过arena进行内存的分配。
      在我们调用threadCache.get()的时候,实则调用的是FastThreadLocal相应的方法:
      在这里插入图片描述
      进行initialize方法
      在这里插入图片描述
      执行子类复写的initialValue
      在这里插入图片描述
      首先从PooledByteBufAllocator维护的heapArenas和directArenas中找到当前支持的线程缓存数最少的PoolArena,然后通过得到的一个heapArena和directArena构建出一个PoolThreadCache对象,因此上边我们可以从threadCache中获取到directArena,这样处理也可以有效的避免多线程申请内存时竞争同一块内存空间的情况
  • 通过PoolArena(DirectArena的allocate)分配
    在这里插入图片描述
    圈中的地方是得到PooledByteBuf的逻辑,内部使用了Recycler,不是重点,先不作分析。重点跟踪PoolArena#allocate(io.netty.buffer.PoolThreadCache, io.netty.buffer.PooledByteBuf, int)
    • 具体allocate逻辑:
    private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
    	//step 1 内存规则化
        final int normCapacity = normalizeCapacity(reqCapacity);
        //step 2 判断申请内存是否小于一个page(8K)
        if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
            int tableIdx;
            PoolSubpage<T>[] table;
            boolean tiny = isTiny(normCapacity);
           // step 3 判断申请内存是否小于512B
           if (tiny) { // < 512
           		//缓存分配tinyPage
                if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
                    // was able to allocate out of the cache so move on
                    return;
                }
                tableIdx = tinyIdx(normCapacity);
                table = tinySubpagePools;
            } else {
            	// 缓存分配smallPage
                if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
                    // was able to allocate out of the cache so move on
                    return;
                }
                tableIdx = smallIdx(normCapacity);
                table = smallSubpagePools;
            }
    
            final PoolSubpage<T> head = table[tableIdx];
    
            /**
             * Synchronize on the head. This is needed as {@link PoolChunk#allocateSubpage(int)} and
             * {@link PoolChunk#free(long)} may modify the doubly linked list as well.
             */
            synchronized (head) {
                final PoolSubpage<T> s = head.next;
                if (s != head) {
                    assert s.doNotDestroy && s.elemSize == normCapacity;
                    long handle = s.allocate();
                    assert handle >= 0;
                    s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
                    incTinySmallAllocation(tiny);
                    return;
                }
            }
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
            }
    
            incTinySmallAllocation(tiny);
            return;
        }
        // 所需内存是否小于等于16M且大于等于一个page
        if (normCapacity <= chunkSize) {
            if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                // was able to allocate out of the cache so move on
                return;
            }
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
                ++allocationsNormal;
            }
        } else {
            // Huge allocations are never served via the cache so just call allocateHuge
            allocateHuge(buf, reqCapacity);
        }
    }
    
    由于本例分配的内存为8k,因此normCapacity <= chunkSize为true,首先会从缓存中读取(第一次不会从缓存读取成功),失败后会走allocateNormal(buf, reqCapacity, normCapacity)分配内存
    • allocateNormal如下 在这里插入图片描述
      先从PoolChunkList中寻找chunk进行分配(从q050开始),初次分配PoolChunkList中均无内存,所以走到新增chunk的逻辑。
      • newChunk会调用底层Unsafe申请内存在这里插入图片描述
      • 申请Chunk之后,执行分配逻辑,即根据要申请内存的容量通过计算拿到chunk中memoryMap的索引返回,如何计算,以及memoryMap的作用稍后分析。
        		 long handle = c.allocate(normCapacity);
        
      • 然后通过申请的chunk初始化PooledByteBuf(即将chunk中的内存片段交由ByteBuf管理)
        		 c.initBuf(buf, handle, reqCapacity);
        
      • 最后将新增的Chunk关联到qInit(PoolChunkList),交由PoolChunkList链管理,返回初始化的PooledByteBuf,流程结束。
        		qInit.add(c);
        

Chunk分配算法分析

我们向操作系统申请的内存是以chunk为单位的(netty默认16M),而事实上,程序中使用内存申请时往往不会直接申请大于等于16M的内存(也有这种可能,相关逻辑在allocateHuge(buf, reqCapacity)),因此我们可以理解为具体的内存分配还需要将chunk切分然后分配。
具体如何进行分配以及如果分配到合适的内存,就不能随性而为了,因为要考虑到内存分配的连续性,要尽量避免内存碎片化。
netty对chunk的分配采用了伙伴分配算法(详情请google):netty中Chunk中持有的连续内存是以完全二叉树的叶子节点表示的,并且二叉树节点状态值交由数组维护。
其中memoryMap[]数组维护某一节点是否使用,可以通过下图来理解
在这里插入图片描述

图片来源

在内存分配过程中,如果要申请一个page(8K)的内存,经过运算会定位到树的第十一层,然后获取一个未被使用过的节点,将节点在数组(memoryMap[])中的下标返回,即可。如果要获取一个16K的内存,那么可以定位到树的第十层,然后获取第十层的一个未被使用的节点,将节点在数组中的下标返回,依次类推。
depthMap[]数组维护节点下标和树深度的关系,该数组的值一经初始化是不会改变的,初始化时 depthMap和memoryMap的长度以及相同下标的值都是相同的,数组的长度是212(4096),相应下标对应的值是当前节点的深度,在depthMap中,如果该节点被分配,那么当前节点对应数组下标的值会被变更为unusable(12),然后当前被分配节点的父节点序号作为下标对应数组中的值会变更为两个子节点中最小的值,递归操作,直到根节点。

Chunk分配代码分析

大概了解了分配模型之后,当然要跟一下代码一探究竟~
来填申请Chunk之后,执行分配逻辑这块的坑~

         	 long handle = c.allocate(normCapacity);
  • 进入到PoolChunk的allocate()
    在这里插入图片描述
    当前例子要分配的内存为8K(一个page),因此会走allocateRun进行分配
    • allocateRun负责分配大于等于一个page的内存
      在这里插入图片描述
      首先根据所需内存通过计算公式计算出当前内存值对应二叉树的层级(比如之前谈到的,所需内存如果是8K的话,会在第十一层【0层为根节点】选取一个节点进行分配,因为第十一层节点内存均按照8K进行划分),具体计算方式可以这么理解:pageShifts默认会被初始化为13,因为树上的一个叶子节点表示8K,213 = 8192 (8K);log2(normCapacity)中,normCapacity是需要分配内存的byte值,如果我们需要分配8K的内存,那么normCapacity就为8192,即213,那么有log2213=13;因此当需要分配8192字节时,(log2(normCapacity) - pageShifts) = 0 ;maxOrder为树的深度,maxOrder - 0 = maxOrder = 11,即当需要分配8192字节时会在第十一层(叶子节点)分配一个节点;当我们需要分配16K即214时,有log2(normCapacity) = 14,即(log2(normCapacity) - pageShifts) = 1,那么有maxOrder - 1 = 10,因此得出分配16K的内存需要再第十层节点进行分配,依次类推,自己体会~
      计算出需要分配树的层级之后,最进行最关键的一步,即在当前层级上找到可以分配的节点!
      • allocateNode(d)分配d层节点
        在这里插入图片描述
      1. 首先id赋值为1,判断当前chunk是否可用,value(id)是获取memoryMap[id]的值,即获取根节点的值,其中val > d (本例val为0,即chunk未被分配),如果成立,那么说明当前chunk已无可以分配当前所需内存的节点。
      2. while循环内部从根节点递归查找可用节点
        id <<= 1:id左移一位,数值上的含义是id * 21,映射到完全二叉树中的含义是取得当前节点的左孩子节点
        val = value(id):取得节点保存的值
        if (val > d):如果当前节点保存的值大于所需分配节点的深度,可知当前节点不具备分配的能力
        id ^= 1:id与1做异或运算,数值上的含义是偶数加一,奇数减一,映射到完全二叉树中就表示为获取当前节点的兄弟节点
        while循环内部寻找分配节点的过程可以概括为:从根节点开始往下寻找二叉树中适合的节点
      3. 将分配的节点设值为unusable(setValue(id, unusable) )
      4. 更新父节点的状态(updateParentsAlloc(id))
        在这里插入图片描述
        id无符号右移一位的数学含义是id/21,反映到完全二叉树中则是,寻找当前节点的父亲节点。 然后拿到当前节点以及当前节点兄弟节点的值,作比较,将最小的赋值给父亲节点。依次,直到更新到根节点。end~

未完成的部分

包括一些子页的分配,各种规格内存的释放,以及对象的回收利用,池化和非池化内存的各种对比等等。
最重要的是要不断加深自身对netty内存管理这块的理解,从而掌握一些普适性的东西。

发布了38 篇原创文章 · 获赞 42 · 访问量 10万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 创作都市 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览