死磕 Netty 之内存篇:再探 Netty 池化内存分配管理

Netty 从 4.1.45 版本开始,基于 jemalloc4.x 算法对内存模块进行重构,两者差别比较大。

内存规格

在文章 深挖 Netty 高性能内存管理 我们知道了基于 jemalloc3 的 Netty将整个内存划分为:Tiny、Small、Normal 和 Huge 四类。其中 Tiny 为 0 ~ 512 B 之间的内存块,Small 为 512B ~ 8KB 之间的内存块,Normal 为 8KB ~ 16M 之间的内存块,Huge 则是大于 16M 的,如下图:

而基于 jemalloc4 的 Netty 则将 Tiny 去掉了,保留了 Small、Normal、Huge,内存划分如下:

  • Small:[0-28K]
  • Normal:(28K - 16M]
  • Huge:> 16M

整体架构

下图是基于 jemalloc3 的 Netty 的内存池架构图:

关于这图的详情,大明哥就不再阐述了,主要是用它来跟基于 jemalloc4 的 Netty 内存结构图对比:

从这个图可以看出,Netty 根据内存模型抽象出来了一些组件:PoolArenaPoolChunkListPoolChunkPoolSubpagePoolThreadCache

下面大明哥在这篇文章就这些组件做一个简单的概括,然后每个组件利用一篇文章来详细介绍,最后用内存分配和释放将所有组件进行一个概括总结,让你彻底掌握 Netty 的内存模块!!

PoolArena 数据结构

PoolArena 是外部申请内存的主要入口,Netty 借据 jemalloc 中 Arena 的设计思想,采用固定数量的多个 Arena 进行内存分配,Arena 的默认数量通常为 CPU 核数 * 2,也可以通过参数 io.netty.allocator.numHeapArenas 来指定,计算规则如下:

 

ini

复制代码

final int defaultMinNumArena = NettyRuntime.availableProcessors() * 2; final int defaultChunkSize = DEFAULT_PAGE_SIZE << DEFAULT_MAX_ORDER; DEFAULT_NUM_HEAP_ARENA = Math.max(0, SystemPropertyUtil.getInt( "io.netty.allocator.numHeapArenas", (int) Math.min( defaultMinNumArena, runtime.maxMemory() / defaultChunkSize / 2 / 3))); DEFAULT_NUM_DIRECT_ARENA = Math.max(0, SystemPropertyUtil.getInt( "io.netty.allocator.numDirectArenas", (int) Math.min( defaultMinNumArena, PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));

线程在首次申请分配内存时,会通过 round-robin 的方式轮询 PoolArena 数组,选择一个固定的 PoolArena ,然后在该线程整个生命周期内就只会与该 PoolArena 打交道,所以每个线程都会保存对应的 PoolArena 信息,从而提高访问效率。

每个线程都会有一个 DirectPoolArena 和一个 HeapArena。

下面是 PoolArena 的数据结构:

 

scala

复制代码

abstract class PoolArena<T> extends SizeClasses implements PoolArenaMetric { static final boolean HAS_UNSAFE = PlatformDependent.hasUnsafe(); // 内存规格 enum SizeClass { Small, Normal } // 所属分配器 final PooledByteBufAllocator parent; final int numSmallSubpagePools; final int directMemoryCacheAlignment; private final PoolSubpage<T>[] smallSubpagePools; private final PoolChunkList<T> q050; private final PoolChunkList<T> q025; private final PoolChunkList<T> q000; private final PoolChunkList<T> qInit; private final PoolChunkList<T> q075; private final PoolChunkList<T> q100; // 省略代码 }

图例如下:

一个 PoolArena 包含了一个 PoolSubpage<T>[] smallSubpagePools 数组和 6 个 PoolChunkList:

  • smallSubpagePools 数组用于存放 Small Subpage类型的内存块。
  • 6 个 PoolChunkList 用于存放使用率不同的 PoolChunk,6 个 PoolChunkList 构成一个双向循环链表。

6 个 PoolChunkList 内存使用情况如下:

每个 PoolChunk 会更加内存使用率的变化在这 6 个 PoolChunkList 来回移动。

PoolChunkList 数据结构

PoolChunkList 管理着多个 PoolChunk,多个使用率相同的 PoolChunk 通过双向链表的方式构建成一个 PoolChunkList,如下:

其定义如下:

 

java

复制代码

final class PoolChunkList<T> implements PoolChunkListMetric { // 所属 PoolArena private final PoolArena<T> arena; private final PoolChunkList<T> nextList; // 最小内存使用率 private final int minUsage; // 最大内存使用率 private final int maxUsage; private final int maxCapacity; private PoolChunk<T> head; private final int freeMinThreshold; private final int freeMaxThreshold; // 省略代码 }

每个 PoolChunkList 都有两个内存使用率的属性:minUsage 和 maxUsage。当 PoolChunk 进行内存分配时,如果内存使用率超过 maxUsage,则从当前的 PoolChunkList 中移除,并添加到下一个 PoolChunkList 中。同时,随着内存的释放,PoolChunk 的内存使用率就会减少,直到小于 minUsage ,则从当前的 PoolChunkList 中移除,并添加到上一个 PoolChunkList 中。

PoolChunk 数据结构

PoolChunk 是 Netty 真正分配内存的地方,一个 PoolChunk 代表 Netty 内存池中一整块的内存,也是 Netty 内存池向 Java 虚拟机申请和释放的最小单位。 其定义如下:

 

java

复制代码

final class PoolChunk<T> implements PoolChunkMetric { private static final int SIZE_BIT_LENGTH = 15; private static final int INUSED_BIT_LENGTH = 1; private static final int SUBPAGE_BIT_LENGTH = 1; private static final int BITMAP_IDX_BIT_LENGTH = 32; static final int IS_SUBPAGE_SHIFT = BITMAP_IDX_BIT_LENGTH; static final int IS_USED_SHIFT = SUBPAGE_BIT_LENGTH + IS_SUBPAGE_SHIFT; static final int SIZE_SHIFT = INUSED_BIT_LENGTH + IS_USED_SHIFT; static final int RUN_OFFSET_SHIFT = SIZE_BIT_LENGTH + SIZE_SHIFT; // 所属 PoolArena final PoolArena<T> arena; final Object base; // 存储的数据 final T memory; // 是否池化 final boolean unpooled; /** * 存储的是有用的run中的第一个和最后一个Page的句柄 */ private final LongLongHashMap runsAvailMap; /** * 管理 PoolChunk 的所有的 Run */ private final LongPriorityQueue[] runsAvail; /** * 管理 PoolChunk 中所有的 PoolSubpage */ private final PoolSubpage<T>[] subpages; private final LongCounter pinnedBytes = PlatformDependent.newLongCounter(); // 一个 page 的大小 private final int pageSize; private final int pageShifts; private final int chunkSize; // 主要是对PooledByteBuf中频繁创建的ByteBuffer进行缓存,以避免由于频繁创建的对象导致频繁的GC private final Deque<ByteBuffer> cachedNioBuffers; int freeBytes; // 所属 PoolChunkList PoolChunkList<T> parent; // 后置节点 PoolChunk<T> prev; // 前置节点 PoolChunk<T> next; // 省略代码 }

PoolChunk 的数据结构比较复杂,其结构图如下:

一个 PoolChunk 由三个部分构成:

  1. Run:一个 Run 由若干个 Page 组成,Page 是 PoolChunk 的分配的最小单位。
  2. Subpage:用于分配 Subpage ,Subpage 的大小为 16B ~ 28K。
  3. free:空闲部分,待分配内存

PoolChunk 还有三个很重要的属性:

  1. runsAvailMap:它存储的是有用的 Run 中的第一个和最后一个 Page 的句柄。它是 runOffset → handle 之间的键值对。
  2. runsAvail:用于管理 PoolChunk 的所有的 Run,它是一个优先队列,每一个队列都管理着相同大小的 Run。
  3. subPages:用于管理 PoolChunk 中所有的 PoolSubpage。

PoolSubpage 数据结构

PoolSubpage 用于分配 Small Subpage,其定义如下:

 

java

复制代码

final class PoolSubpage<T> implements PoolSubpageMetric { // 所属 PoolChunk final PoolChunk<T> chunk; // 每块内存的大小 final int elemSize; // 页面偏移量 private final int pageShifts; // PoolSubpage 在 PoolChunk 中 memory 的偏移量 private final int runOffset; // Run 的大小 private final int runSize; // 每一小块内存的状态 private final long[] bitmap; // 前置节点 PoolSubpage<T> prev; // 后置节点 PoolSubpage<T> next; boolean doNotDestroy; // 最多可以存放多少小内存块 private int maxNumElems; private int bitmapLength; private int nextAvail; private int numAvail; // 省略代码 }

其结构如下:

  • 26
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值