网络编程20

ByteBuf和ByteBufAllocator分析

  • 原生jdk的buffer缺点,一是定长,二是需要切换读写模式

  • netty在jdk中Buffer的基础上提出了ByteBuf,本质上也是byte数组的缓冲区

  • 怎么实现ByteBuf更好?

    • 1.门面模式
    • 2.完全重写
    • 两种模式都用了

ByteBuf抽象类的继承关系

  • 顶层是ByteBuf抽象类

  • 2.AbstractByteBuf抽象类

    • 公共方法都放在这里
  • 3.AbstractReferenceCountedByteBuf抽象类

    • 引用计数
    • 对byteBuf这个对象的分配和销毁进行跟踪,以做自动的内存回收
  • 4.1PooledByteBuf抽象类

    • 基于内存池的byteBuf,不是用完了就丢弃了,而是保存起来,下次再要用直接从内存池中去拿,实现了复用
    • 维持了一个内存池,减少了gc
    • 但是管理和维护很复杂,使用要谨慎
    • ##byteBuf分类方法有3种##除了基于内存分配、基于内存回收分类,还有一类,对内存的操作方式,平时拿一个对象是通过new Object(),而jdk-Unsafe可以直接操作内存
  • 4.2UnpooledDirectByteBuf实体类

    • 基于直接内存
    • 临时去找操作系统申请的,分配回收速度要慢于堆
  • 4.3UnpooledHeapByteBuf实体类

    • 基于堆内存
    • jvm中的堆,它的内存回收和分配分配非常快,而且那块内存区域可以被jvm自动回收
    • 缺点相比于直接内存多了一次拷贝过程,发送数据时,要将数据从堆拷贝到应用程序缓冲区,再从应用程序缓冲区到套接字发送缓冲区,使用直接内存就不用从堆上拷贝到应用程序缓冲区
  • 5.1PooledHeapByteBuf实体类

  • 5.2PooledDirectByteBuf实体类

  • 5.3PooledUnsafeDirectByteBuf实体类

Unpooled

  • 通过ByteBufAllocator进行内存分配的

ByteBufAllocator

  • 内部方法规定了可以拿到什么类型的buffer
    • directBuffer
    • heapBuffer
    • ioBuffer:尽可能分配直接内存,系统不支持则分配堆内存
  • ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR
    • 缺省时,安卓类型是非池化的,其余都是池化类型的,所以linux系统缺省时是池化的

PreferHeapByteBufAllocator

  • 实现了ByteBufAllocator接口
  • netty打了@UnstableApi注解,这一个实现是不稳定的

AbstractByteBufAllocator

  • 是一个抽象类

  • 也实现了ByteBufAllocator接口,是它的骨架实现类

  • 它提供了两个比较重要的抽象方法,newHeapBuffer和newDirectBuffer,供子类去实现到底是从堆还是直接内存上的byteBuffer

  • 里面相关接口的实现,最后都归结与要对上述两个抽象方法的实现,只是定义了一些规范

  • 堆内存实现----UnpooledByteBufAllocator中,最终是以字节数组保存的,只判断是否有unSafe类

  • 直接内存实现----UnpooledByteBufAllocator,最终是用的jdk中的ByteBuffer保存数据的,除了判断是由unSafe类,还判断了noCleaner,与jdk对直接内存的回收有关,对于直接内存的回收,jdk提供了两种方式

    • 在DirectByteBuffer中提供了一个cleaner参数,专门来进行直接内存的回收,除了这种,jdk还提供了unsafe freeMemory来进行直接内存的回收,所以分配内存方式多了一种,有三种

UnpooledByteBufAllocator

  • 是非池化的,没当需要使用都是从内存中临时申请的

  • newHeapBuffer

    • PlatformDependent.hasUnsafe(),通过反射机制去判断是否可以拿到jdk底层的Unsafe对象,如果拿得到,直接通过Unsafe去操作内存,如果拿不到,则通过new这种形式去拿

      (1).InstrumentedUnpooledUnsafeHeapByteBuf,也是用的直接数组来保存要发送的数据

      (2).InstrumentedUnpooledHeapByteBuf

      $1.继承自UnpooledHeapByteBuf

      $2.覆盖了allocateArray方法,最终在UnpooledHeapByteBuf中维护的是一个字节数组

PooledByteBufAllocator

  • 池化实现的
  • newHeapBuffer,内部保存的还是字节数组
    • 由于是池化的,不会看到new对象这种形式,而是首先从缓冲区里面获取PoolArea对象,然后由这个对象再去拿相关的buffer
  • newDirectBuffer,内部还是通过jdk的ByteBuffer保存

AbstractByteBuf主要做的事情

  • 一些公共的属性和功能进行定义和实现

    • 读索引和写索引,readIndex和writeIndex

    • public ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) 
      

      1.读取当前byteBuf

      2.首先检查当前byteBuf里有多少可以读的东西

      3.调用getBytes,从当前读索引开始,复制多少个字节到dst目标字节数组中去

      $getBytes

      是一个抽象方法,交给具体的子类去实现

      4.对读索引进行增加

    • public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length)
      

      1.首先判定当前有足够的空间可以写

      2.调用setBytes,从当前写索引开始,读取指定长度的数据往指定数组中去写

      3.对写索引进行相关的累加

  • ByteBuf如何进行扩容?---- ensureWritable

    • 在ensureWritable0方法中,就有一个所谓的扩容

    • 检查写的数据写完后,会不会ByteBuf的最大容量,如果超过了,会抛出一个异常,如果没有超过,netty实现扩容时采用的是一种步进的扩容算法

      // 扩容
      public int calculateNewCapacity(int minNewCapacity, int maxCapacity) {
              
              
          if (minNewCapacity < 0) {
              
              
              throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expected: 0+)");
          }
          if (minNewCapacity > maxCapacity) {
              
              
              throw new IllegalArgumentException(String.format(
                  "minNewCapacity: %d (expected: not greater than maxCapacity(%d)",
                  minNewCapacity, maxCapacity));
          }
          // 扩容阈值,4M,经验值
          final int threshold = CALCULATE_THRESHOLD; // 4 MiB page
      
          if (minNewCapacity == threshold) {
              
              
              return threshold;
          }
      
          // If over threshold, do not double but just increase by threshold.
          if (minNewCapacity > threshold) {
              
              
              int newCapacity = minNewCapacity / threshold * threshold;
              if (newCapacity > maxCapacity - threshold) {
              
              
                  newCapacity = maxCapacity;
              } else {
              
              
                  newCapacity += threshold;
              }
              return newCapacity;
          }
      
          // Not over threshold. Double up to 4 MiB, starting from 64.
          int newCapacity = 64;
          // 如果在4M以下,按照2的倍数来进行扩容
          // 内存小的时候乘2,内存浪费小,同时可以避免内存反复扩容
          // 大于4M,还扩充这么大会造成内存的浪费
          // 4M之后,每次只加4M
          while (newCapacity < minNewCapacity) {
              
              
              newCapacity <<= 1;
          }
      
          return Math.min(newCapacity, maxCapacity);
      }
      

AbstractReferenceCountedByteBuf

  • 使用cas(AtomicIntegerFieldUpdater)对计数相关部分保证是线程安全的,还使用了private volatile int refCnt这样一个volatile的变量,来保证操作后的可见性
  • retain方法
    • 每调用一次,计数器就会进行增加1
  • release方法
    • 每调用一次,计数器就会扣减1

UnpooledHeapByteBuf

  • 每次保证数据都会创建一个新的,不太容易出现内存管理问题,如果不是特别追求性能,采用这个更好一点

  • 成员变量定义了一个ByteBufAllocator分配器,同时底层使用一个byte数组作为保存数据的缓冲区,而且还定义了一个tmpNioBuf(ByteBuffer类型),netty是基于jdk的nio封装的,jdk是不认得netty中的ByteBuf的,tmpNioBuf专门用来中转的,ByteBuffer底层也是字节数组实现的,也提供直接将一个字节数组包装成ByteBuffer的方法,

  • 不管setBytes还是getBytes,都是对那个字节数组进行操作

UnpooledDirectByteBuf

  • 其实和UnpooledHeapByteBuf差不多,唯一不同的保证数据用的是ByteBuffer这一个jdk的原生API,同样的,setBytes还是getBytes,都是围绕ByteBuffer

PooledByteBuf

  • 是一个抽象类
  • 跟unpooled相关的buffer,没有太大的不同,关键在于内存的池化

Jemalloc算法

  • 使用了一种关于内存池的算法,并维护相关内存池

  • 这个算法是在操作系统FreeBSD提出的

  • 基本思路:比如说买东西,有时候买的东西,看到它有的时候从住的城市本地出库,有的时候又从其他地方出库,为什么要这么做?放在本地仓库,一般是比较小的东西,对于区域仓库,稍微大点的东西,比如电视,而对于全国仓库,就是汽车这种更大的东西

    • netty里面分配内存最小是16B

      netty把申请内存大小做了分级:

      (1)x<512B tiny

      tiny里面按16B逐步递增,按16累加,也就是16,32,48,…496,有32种

      (2)512<x<8KB small

      翻倍增加,512KB,1KB,2KB,4KB,有4种

      (3)8KB<x<16MB normal

      翻倍增加,

      (4)x>16MB huge

      翻倍增加

    • 假设申请511B,netty给512B

      申请513B,netty给1KB,即向上取

    • 在netty里面,对于tiny和small,从本地仓库去拿,在netty中是ThreadCache,如果没有,或者是normal类型,则从华中仓库出库,在netty中是PoolArena,对于全国仓库,也就是huge类型,池化里面不保留,直接从系统内存中去申请

    • PoolArena其实还是很大,还会切分,切分成一个一个的ChunK,一个Chunk是16M,一个PoolArena里面会有很多的Chunk,这些Chunk根据它们的内存使用率,在0~25放到QINIT中,在0-50放到Q0中,在25-75放到Q25,在50-100放到Q50,在75-100放到Q75,在100放到Q100,而这些Q0、Q25之间等等是通过双向链表组织起来的,而每一个Q0、Q25、…内部中的poolchunk也是通过双向链表连接起来的,当Q25中的poolchunk的内存使用率超过50%,就会被摘下来,放到Q50中去

    • 一个PoolChunk是16MB,其实还是很大,为了进一步提高内存使用率,减少内存碎片,会把PoolChunk继续切分,切分成一个一个的page,总共又2048个,一个page大概是8KB,把这些page组合成满二叉树,一共有12层。假如说现在往二叉树上分配内存,分别是8k,16k,8k,因为相关定义是第0层一定是16M,第11层一定是8k,根节点是1,放在第0层,节点2048在第11层,所以2048这个节点每一个是8K,则在第10层,1024节点所包含的内存是16KB,而现在要分配16KB,直接把1025这个节点分配出去,第三个8KB则分配到2049这个节点上,分配了三次,内存还是连接在一起的

    • 为了实现这种伙伴算法,netty在PoolChunk里面专门定义了两个字节数组memoryMap(存放分配信息)和depthMap(节点的高度信息)来指明当前page有没有分配出去,为什么用数组就能实现满二叉树?满二叉树不需要声明左右节点来表示,假设根节点从1开始,现在有一个节点k,则它的左孩子是2k,右孩子是2k+1,相反也能找到父节点

      $memoryMap

      (1).初始化时,数组中默认的元素默认是层数,比如memoryMap[0]=1,memoryMap[2048]=11

      (2).开始进行分配,假设memoryMap[4]=2,假设4下面所有的内存区域都被分配了,就把memoryMap[4]改成12,因为总共只有12层,说明4号节点以下的内存都不可用了,同时更新4号节点的父节点2,把2号节点的值改成4号节点和它的兄弟节点中保存值的较小值,即把memoryMap[2]由1改成2,同理,把根节点memoryMap[1]由0改成1,这么改有什么好处?对于任意节点x,如果memoryMap[x]==depthMap[x],则说明x节点以下没有任何一个page被分配出去,如果memoryMap大,说明x节点以下至少有一个节点被分配出去了,所以此时2号节点无法再分配出8MB内存了,如果需要一次性8MB内存,只能去3号节点找了

      $depthMap

      (1)depthMap的初始化值和memoryMap相同,都是层数,比如depthMap[0]=1,depthMap[2048]=11

      (2)开始分配后,depthMap的值也仍然保持不变

    • 一个page是8KB,而netty中最小的是16B,如果直接分配8KB,还是很浪费,所以在netty里面对于每一个page还进一步进行了切分,切分成subpage(子页),这个subpage就是最小单位了,但是要注意subpage的大小是不固定的,比如说,现在要申请32B的内存,拿一个page,从这个page上切割一个subpage下来,这个subpage是32B,从此以后,这个page永远按照32B进行切分

      $PoolArena

      (1)针对subpage,PoolArena在这个类里面会有两个专门的PoolSubpage类型的数组tinySubpagePools和smallSubpagePools,每一个组都是单独的大小,tinySubpagePools,按16B,最多有32种类型,smallSubpagePools最多有4种类型,这个数组里面最多有32个元素,每一个元素就代表一种内存大小,把相同大小的内存块用链表串联起来,方便netty管理,如果16B的一个小块用完了,可以快速的删除

PoolArena

  • qinit

  • q0

  • q1

  • q100

  • 注意:PoolArena并不是整个netty只有一个,eventloop每一个线程都要去内存池种拿相关的内存来保存数据,默认数量与处理器的个数相当,8核就有8个PoolArena,eventloop的默认个数是cpu*2,所以PoolArena会存在竞争,所以netty里面实际去拿内存区域时,会去ThreadCache中拿,在netty中的类就是PoolThreadCache,刚开始启动时,会去PoolThreadCache中拿,如果拿不到就会先去PoolArena申请,申请到了,用过后realase时会放到PoolThreadCache中,下次再申请时先从PoolThreadCache中去找,所以在PoolThreadCache中也进行了区分,分成tinySubPageHeapCaches、smallSubPageHeapCaches、tinySubPageDirectCaches、smallSubPageDirectCaches、normalHeapCaches、normalDirectCaches

q100

  • 每一级都是一个大链表

    • 1.而这个大链表是由一个一个的PoolChunk组成的

    • 2.poolchunk里面采用二叉树的形式来维护一个一个的page,每个page是8KB的大小

    • 3.如果申请的内存是tiny或者small类型的,又会把page拆分成subpage进行相关的维护

PoolThreadCache

  • tiny下最多有32个
  • small下最多有4个
  • normal下面最多有3个

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/114650921