昨天,我彻底搞懂了Netty内存分配策略!
Posted 51CTO技术栈
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了昨天,我彻底搞懂了Netty内存分配策略!相关的知识,希望对你有一定的参考价值。
Netty 作为一款高性能的 RPC 框架必然涉及到频繁的内存分配销毁操作。
图片来自 Pexels
如果是在堆上分配内存空间将会触发频繁的 GC,JDK 在 1.4 之后提供的 NIO 也已经提供了直接直接分配堆外内存空间的能力,但是也仅仅是提供了基本的能力,创建、回收相关的功能和效率都很简陋。
基于此,在堆外内存使用方面,Netty 自己实现了一套创建、回收堆外内存池的相关功能。基于此我们一起来看一下 Netty 是如何实现内存分配的。
Netty 中的数据容器分类
谈到数据保存肯定要说到内存分配,按照存储空间来划分,可以分为堆内存和堆外内存;按照内存区域连贯性来划分可以分为池化内存和非池化内存。这些划分在 Netty 中的实现接口分别如下。
按照底层存储空间划分:
堆缓冲区:HeapBuffer
直接缓冲区:DirectBuffer
按照是否池化划分:
池化:PooledBuffer
非池化:UnPooledBuffer
默认使用 PoolDireBuf 类型的内存,这些内存主要由 PoolArea 管理。另外 Netty 并不是直接对外暴露这些 API,提供了 Unsafe 类作为出口暴露数据分配的相关操作。
什么是池化?
一般申请内存是检查当前内存哪里有适合当前数据块大小的空闲内存块,如果有就将数据保存在当前内存块中。
池化解决的问题:内存碎片。
比如固定申请 1M 的空间作为某个线程的使用内存,但是该线程每次最多只占用 0.5M,那么每次都有 0.5M 的碎片。如果该空间不被有效回收时间一长必然存在内存空洞。
外碎片:是指多个内存空间合并的时候发现不够分配给待使用的空间大小。
比如有一个 20byte,13byte 的连续内存空间可以被回收,现在有一个 48byte 的数据块需要存储,而这两个加起来也只有 33byte 的空间,必然不会被使用到。
如何实现内存池?
这种方式实现简单,但是搜索和释放内存维护的难度还是比较大,不太适合。
②定长内存空间分配
维护两个列表,一个是未分配内存列表,一个是已分配内存列表。每个内存块都是一样大小,分配时如果不够就将多个块合并到一起。
这种方式的缺点就是会浪费一定的内存空间,如果有特定的场景还是没有问题。
③多段定长池分配
在上面的定长分配基础上,由原来的固定一个长度分配空间变为按照不同对象大小(8,16,32,64,128,256,512,1k…64K),的方式分配多个固定大小的内存池。
每次要申请内存的时候按照当前对象大小去对应的池中查找是否有剩余空间。
malloc/free 的实现过程:
当有申请请求时,malloc 会扫描空闲链表,直到找到一个足够大的块为止。(首次适应)(因此每次调用 malloc 时并不是花费了完全相同的时间)
如果该块恰好与请求的大小相符,则将其从链表中移走并返回给用户。如果该块太大,则将其分为两部分,尾部的部分分给用户,剩下的部分留在空闲链表中(更改头部信息)。因此 malloc 分配的是一块连续的内存。
释放时首先搜索空闲链表,找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合为一个更大的块,以减少内存碎片。
Netty 中的内存分配
tiny:代表了大小在 0-512B 的内存块。
small:代表了大小在 512B-8K 的内存块。
normal:代表了大小在 8K-16M 的内存块。
huge:代表了大于 16M 的内存块。
Chunk:一个 Chunk 的大小是 16M,Chunk 是 Netty 对操作系统进行内存申请的单位,后续所有的内存分配都是在 Chunk 里面进行操作。
Page:Chunk 内部以 Page 为单位分配内存,一个 Page 大小为 8K。当我们需要 16K 的空间时,Netty 就会从一个 Chunk 中找到两个 Page 进行分配。
Subpage 和 element:element 是比 Page 更小的单位,当我们申请小于 8K 的内存时,Netty 会以 element 为单位进行内存分配。element 没有固定大小,具体由用户的需求决定。
Netty 通过 Subpage 管理 element,Subpage 是由 Page 转变过来的。当我们需要 1K 的空间时,Netty 会把一个 Page 变成 Subpage,然后把 Subpage 分成 8 个 1K 的 element 进行分配。
Chunk 中的内存分配
Tiny:小于 512 的情况,最小空间为 16,对齐大小为 16,区间为[16,512),所以共有 32 种情况。
Small:大于等于 512 的情况,总共有四种:512,1024,2048,4096。
tinySubpagePools
smallSubpagePools
一系列的 PoolChunkList
PoolChunkList 是一个容器,其内部可以保存一系列的 PoolChunk 对象,并且,Netty 会根据内存使用率的不同,将 PoolChunkList 分为不同等级的容器。
abstract class PoolArena<T> implements PoolArenaMetric {
enum SizeClass {
Tiny,
Small,
Normal
}
// 该参数指定了tinySubpagePools数组的长度,由于tinySubpagePools每一个元素的内存块差值为16,
// 因而数组长度是512/16,也即这里的512 >>> 4
static final int numTinySubpagePools = 512 >>> 4;
//表示该PoolArena的allocator
final PooledByteBufAllocator parent;
//表示PoolChunk中由Page节点构成的二叉树的最大高度,默认11
private final int maxOrder;
//page的大小,默认8K
final int pageSize;
// 指定了叶节点大小8KB是2的多少次幂,默认为13,该字段的主要作用是,在计算目标内存属于二叉树的
// 第几层的时候,可以借助于其内存大小相对于pageShifts的差值,从而快速计算其所在层数
final int pageShifts;
//默认16MB
final int chunkSize;
// 由于PoolSubpage的大小为8KB=8196,因而该字段的值为
// -8192=>=> 1111 1111 1111 1111 1110 0000 0000 0000
// 这样在判断目标内存是否小于8KB时,只需要将目标内存与该数字进行与操作,只要操作结果等于0,
// 就说明目标内存是小于8KB的,这样就可以判断其是应该首先在tinySubpagePools或smallSubpagePools
// 中进行内存申请
final int subpageOverflowMask;
// 该参数指定了smallSubpagePools数组的长度,默认为4
final int numSmallSubpagePools;
//tinySubpagePools用来分配小于512 byte的Page
private final PoolSubpage<T>[] tinySubpagePools;
//smallSubpagePools用来分配大于等于512 byte且小于pageSize内存的Page
private final PoolSubpage<T>[] smallSubpagePools;
//用来存储用来分配给大于等于pageSize大小内存的PoolChunk
//存储内存利用率50-100%的chunk
private final PoolChunkList<T> q050;
//存储内存利用率25-75%的chunk
private final PoolChunkList<T> q025;
//存储内存利用率1-50%的chunk
private final PoolChunkList<T> q000;
//存储内存利用率0-25%的chunk
private final PoolChunkList<T> qInit;
//存储内存利用率75-100%的chunk
private final PoolChunkList<T> q075;
//存储内存利用率100%的chunk
private final PoolChunkList<T> q100;
//堆内存(heap buffer)
static final class HeapArena extends PoolArena<byte[]> {
}
//堆外直接内存(direct buffer)
static final class DirectArena extends PoolArena<ByteBuffer> {
}
}
qInit:存储内存利用率 0-25% 的 chunk
q000:存储内存利用率 1-50% 的 chunk
q025:存储内存利用率 25-75% 的 chunk
q050:存储内存利用率 50-100% 的 chunk
q075:存储内存利用率 75-100%的 chunk
q100:存储内存利用率 100%的 chunk
下面来看 PoolArena 是如何分配内存的:
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
// 将需要申请的容量格式为 2^N
final int normCapacity = normalizeCapacity(reqCapacity);
// 判断目标容量是否小于8KB,小于8KB则使用tiny或small的方式申请内存
if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
int tableIdx;
PoolSubpage<T>[] table;
boolean tiny = isTiny(normCapacity);
// 判断目标容量是否小于512字节,小于512字节的为tiny类型的
if (tiny) { // < 512
// 将分配区域转移到 tinySubpagePools 中
if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) {
// was able to allocate out of the cache so move on
return;
}
// 如果无法从当前线程缓存中申请到内存,则尝试从tinySubpagePools中申请,这里tinyIdx()方法
// 就是计算目标内存是在tinySubpagePools数组中的第几号元素中的
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;
} else {
// 如果目标内存在512byte~8KB之间,则尝试从smallSubpagePools中申请内存。这里首先从
// 当前线程的缓存中申请small级别的内存,如果申请到了,则直接返回
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];
// 这里需要注意的是,由于对head进行了加锁,而在同步代码块中判断了s != head,
// 也就是说PoolSubpage链表中是存在未使用的PoolSubpage的,因为如果该节点已经用完了,
// 其是会被移除当前链表的。也就是说只要s != head,那么这里的allocate()方法
// 就一定能够申请到所需要的内存块
synchronized (head) {
// s != head就证明当前PoolSubpage链表中存在可用的PoolSubpage,并且一定能够申请到内存,
// 因为已经耗尽的PoolSubpage是会从链表中移除的
final PoolSubpage<T> s = head.next;
// 如果此时 subpage 已经被分配过内存了执行下文,如果只是初始化过,则跳过该分支
if (s != head) {
// 从PoolSubpage中申请内存
assert s.doNotDestroy && s.elemSize == normCapacity;
// 通过申请的内存对ByteBuf进行初始化
long handle = s.allocate();
assert handle >= 0;
// 初始化 PoolByteBuf 说明其位置被分配到该区域,但此时尚未分配内存
s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
// 对tiny类型的申请数进行更新
if (tiny) {
allocationsTiny.increment();
} else {
allocationsSmall.increment();
}
return;
}
}
// 走到这里,说明目标PoolSubpage链表中无法申请到目标内存块,因而就尝试从PoolChunk中申请
allocateNormal(buf, reqCapacity, normCapacity);
return;
}
// 走到这里说明目标内存是大于8KB的,那么就判断目标内存是否大于16M,如果大于16M,
// 则不使用内存池对其进行管理,如果小于16M,则到PoolChunkList中进行内存申请
if (normCapacity <= chunkSize) {
// 小于16M,首先到当前线程的缓存中申请,如果申请到了则直接返回,如果没有申请到,
// 则到PoolChunkList中进行申请
if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
// was able to allocate out of the cache so move on
return;
}
allocateNormal(buf, reqCapacity, normCapacity);
} else {
// 对于大于16M的内存,Netty不会对其进行维护,而是直接申请,然后返回给用户使用
allocateHuge(buf, reqCapacity);
}
}
如果目标容量小于 16 字节,则返回 16。
如果目标容量大于 16 字节,小于 512 字节,则以 16 字节为单位,返回大于目标字节数的第一个 16 字节的倍数。比如申请的 100 字节,那么大于 100 的 16 整数倍最低为:16*7=112,因而返回 112。
如果目标容量大于 512 字节,则返回大于目标容量的第一个 2 的指数幂。比如申请的 1000 字节,那么返回的将是:2^10 = 1024。
①PoolSubpage 用于分配小于 8k 的内存
tinySubpagePools:用于分配小于 512 字节的内存,默认长度为 32,因为内存分配最小为 16,每次增加 16,直到 512,区间 [16,512) 一共有 32 个不同值。
smallSubpagePools:用于分配大于等于 512 字节的内存,默认长度为 4。tinySubpagePools 和 smallSubpagePools 中的元素默认都是 subpage。
②poolChunkList 用于分配大于 8k 的内存
上面已经解释了 q 开头的几个变量用于保存大于 8k 的数据。
如果分配一个 page 以上的内存,直接采用方法 allocateNormal() 分配内存,allocateNormal() 则会将申请动作交由 PoolChunkList 进行。
private synchronized void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
//如果在对应的PoolChunkList能申请到内存,则返回
if (q050.allocate(buf, reqCapacity, normCapacity) || q025.allocate(buf, reqCapacity, normCapacity) ||
q000.allocate(buf, reqCapacity, normCapacity) || qInit.allocate(buf, reqCapacity, normCapacity) ||
q075.allocate(buf, reqCapacity, normCapacity)) {
++allocationsNormal;
return;
}
// Add a new chunk.
PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
long handle = c.allocate(normCapacity);
++allocationsNormal;
assert handle > 0;
c.initBuf(buf, handle, reqCapacity);
qInit.add(c);
}
如下是 free() 方法的源码:
public void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity,
PoolThreadCache cache) {
// 如果是非池化的,则直接销毁目标内存块,并且更新相关的数据
if (chunk.unpooled) {
int size = chunk.chunkSize();
destroyChunk(chunk);
activeBytesHuge.add(-size);
deallocationsHuge.increment();
} else {
// 如果是池化的,首先判断其是哪种类型的,即tiny,small或者normal,
// 然后将其交由当前线程的缓存进行处理,如果添加成功,则直接返回
SizeClass sizeClass = sizeClass(normCapacity);
if (cache != null && cache.add(this, chunk, nioBuffer, handle,
normCapacity, sizeClass)) {
return;
}
// 如果当前线程的缓存已满,则将目标内存块返还给公共内存块进行处理
freeChunk(chunk, handle, sizeClass, nioBuffer);
}
}
编辑:陶家龙
精彩文章推荐:
以上是关于昨天,我彻底搞懂了Netty内存分配策略!的主要内容,如果未能解决你的问题,请参考以下文章