本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!
大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。
本文已收录到我的技术网站:www.skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经
本篇文章我们再来探索 Netty 的池化内存分配管理机制,其实在文章 深挖 Netty 高性能内存管理 已经讲述过一次了,但是那篇文章是基于 jemalloc3 版本,而这篇内容则是基于 jemalloc4 版本(包括后面的源码部分),也是大明哥在写这个系列之前的最新版本 4.1.77.Final。
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 根据内存模型抽象出来了一些组件:PoolArena
、PoolChunkList
、PoolChunk
、PoolSubpage
、PoolThreadCache
。
下面大明哥在这篇文章就这些组件做一个简单的概括,然后每个组件利用一篇文章来详细介绍,最后用内存分配和释放将所有组件进行一个概括总结,让你彻底掌握 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 由三个部分构成:
- Run:一个 Run 由若干个 Page 组成,Page 是 PoolChunk 的分配的最小单位。
- Subpage:用于分配 Subpage ,Subpage 的大小为 16B ~ 28K。
- free:空闲部分,待分配内存
PoolChunk 还有三个很重要的属性:
- runsAvailMap :它存储的是有用的 Run 中的第一个和最后一个 Page 的句柄。它是
runOffset → handle
之间的键值对。 - runsAvail:用于管理 PoolChunk 的所有的 Run,它是一个优先队列,每一个队列都管理着相同大小的 Run。
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;
// 省略代码
}
其结构如下:
由于篇幅和内容问题,本篇文章就结束了,后面大明哥就这些组件做详细的介绍!!