简介
Netty内存主要分为两种: DirectByteBuf和HeapByteBuf, 实际上就是堆外内存和堆内内存。堆外内存又称直接内存, 通过io.netty.noPreferDirect参数设置。 自从JDK1.4开始, 增加了NIO, 可以直接Native函数在堆外构建直接内存。Netty作为服务器架构技术, 拥有大量的网络数据传输, 当我们进行网络传输时, 必须将数据拷贝到直接内存, 合理利用好直接内存, 能够大量减少堆内数据和直接内存考虑, 显著地提高性能。 但是堆外内存也有一定的缺点, 它进程主动垃圾回收,垃圾回收效率也极低, 因此, netty主动创建了Pool和Unpool的概念。
Pool和Unpool区别
字面意思, 分别是池化内存和非池化内存。池化内存
的管理方式是首先申请一大块内存, 然后再慢慢使用, 当使用完成释放后, 再将该部分内存放入池子中, 等待下一次的使用, 这样的话, 可以减少垃圾回收次数, 提高处理性能。非池化内存
就是普通的内存使用, 需要时直接申请, 释放时直接释放。 可以通过参数Dio.netty.allocator.type
确定netty默认使用内存的方式, 目前netty针对pool做了大量的支持, 这样内存使用直接交给了netty管理, 减轻了直接内存回收的压力。 所以在netty4时候, 默认使用pool方式。
这样的话, 内存分为四种: PoolDireBuf、UnpoolDireBuf、PoolHeapBuf、UnpoolHeapBuf。netty底层默认使用的PoolDireBuf类型的内存, 这些内存主要由PoolArea管理, 这也是本文的重点。
内存分配
线程调用如下接口来获取内存:
1 | protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { |
主要做的事:
- 获取该线程绑定的PoolThreadCache(可参考Netty PoolThreadCache原理探究)
- 从绑定的PoolThreadCache中获取PoolArena, 从PoolArena中开始真正分配内存。
注意这里的一个细节:
- 若通过非池申请的直接内存, 使用的是ByteBuffer.allocateDirect(maxCapacity)分配内存, 那么我们可以通过ManagementFactory.getPlatformMXBeans()方式获取到该内存块的大小。这样申请的堆外内存大小受参数XX:MaxDirectMemorySize控制。
- 若以内存池的方式申请内存, 使用的是unsafe.allocateMemory(size)方式申请内存, 此块内存已不再jvm管理范围之类, 我们不能再通过ManagementFactory.getPlatformMXBeans()方式获取该内存大小, 在netty中, 是通过PlatformDependent.DIRECT_MEMORY_COUNTER来统计的。
PoolArena
PoolArena作为Netty底层内存池核心管理类, 主要原理是首先申请一些内存块, 不同的成员变量来完成不同大小的内存块分配。下图描述了Netty最重要的成员变量:
netty将池化内存块划分为3个类型:
1 | enum SizeClass { |
Tiny主要解决16b-498b之间的内存块分配, small解决分配512b-4kb的内存分配, normal解决8k-16m的内存分配。
大致了解了这些, 为了更详细的了解分配细节, 首先对PoolArena成员变量进行简单分析。
1 | //tiny级别的个数, 每次递增2^4b, tiny总共管理32个等级的小内存片:[16, 32, 48, ..., 496], 注意实际只有31个级别内存块 |
PoolArea申请内存时根据申请的大小使用不同对象进行分配:
- tinySubpagePools分配[16b, 496b]之间的内存大小, 数组中每个元素以16b为一个单位增长, 比如申请分配16b的内存, 将在下标为0对应的链中分配; 申请32b的内存, 将在下标为1对应的链中分配。
- smallSubpagePools分配[512b, 4k]之间的内存大小, 分配结构同tinySubpagePools一样。
- q050、q025、q000、qInit、q075主要负责分配[8k, 16M]大小的内存, 其存放的元素都是大小为16M的PoolChunk, 这几个成员变量不同的是元素PoolChunk的使用率不同, 比如q025里面存放的chunk使用率为[25%, 75%]。 若需要申请[16b, 4k]的内存、而tinySubpagePools、smallSubpagePools没有合适的内存块时, 会从这些对象包含的PoolChunk中分配8k的叶子节点供重新划分结构进行分配。
他们存储的属性PoolChunk可以在不同的属性中移动, 其中:
若q025中某个PoolChunk使用率大于25%之后, 该PoolChunk将别移动到q050中。
若q050中某个PoolChunk使用率小于50%之后, 该PoolChunk将别移动到q025中。
若qInit使用率为0, 也不会释放该节点。
若q000使用率为0, 会被释放掉。
numThreadCaches负责统计该PoolChunk被多少NioEventLoop线程绑定, 具体可见Netty PoolThreadCache原理探究
PoolArena的内存分配
线程分配内存主要从两个地方分配: PoolThreadCache和PoolArena
其中PoolThreadCache线程独享, PoolArena为几个线程共享。
netty真正申请内存时, 首先便是调用PoolArena.allocate()函数:
1 | private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) { |
PoolArena.allocate()分配内存主要考虑先尝试从缓存中, 然后再尝试从PoolArena分配。tiny和small申请过程一样, 以下都以tiny申请为例。具体过程如下:
1). 对申请的内存进行规范化, 就是说只能申请某些固定大小的内存, 比如tiny范围的16b倍数的内存, small范围内512b, 1k, 2k, 4k范围内存, normal范围内8k, 16k,…, 16m范围内内存, 始终是2幂次方的数据。申请的内存不足16b的,按照16b去申请。
2). 判断是否是小于8K的内存申请, 若是申请Tiny/Small级别的内存:
- 首先尝试从cache中申请, 具体申请过程参考Netty PoolThreadCache原理探究
- 若在cache中申请不到的话, 接着会尝试从tinySubpagePools中申请, 首先计算出该内存在tinySubpagePools中对应的下标, 下标计算公式如下:
1
2
3
4
5
6
7
8
9
10
11static int tinyIdx(int normCapacity) { //申请内容小于512,下标
return normCapacity >>> 4; //在tiny维护的链中找到合适自己位置的下标, 除以16,就是下标了
}
static int smallIdx(int normCapacity) {
int tableIdx = 0;
int i = normCapacity >>> 10; //首先是512 = 2^10
while (i != 0) {
i >>>= 1;
tableIdx ++;
}
return tableIdx;
可以看出, normCapacity/16就是tiny级别的下标, normCapacity/1024就是small级别的下标。 然后再获取tinySubpagePools对应级别的内存的头结点head。
- 检查对应链串是否已经有PoolSubpage可用, 若有的话, 直接进入PoolSubpage.allocate进行内存分撇, 具体可见Netty-PoolSubpage原理探究, 并且根据handle初始化这块内存块。
- 若没有可分配的内存, 则会进入allocateNormal进行分配
3). 若分配normal类型的类型, 首先也会尝试从缓存中分配, 然后再考虑从allocateNormal进行内存分配。
4). 若分配大于16m的内存, 则直接通过allocateHuge()从内存池外分配内存。
分配[16b, 16m]内存
接着上述过程, 会进入allocateNormal进行内存分配
1 | private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) { |
- 首先会依次检查q050、q025、q000、qInit、q075链中的PoolArea, 是否能否分配该大小的内存, 检查分配过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
if (head == null || normCapacity > maxCapacity) { //head是可以直接寸数据的
// Either this PoolChunkList is empty or the requested capacity is larger then the capacity which can
// be handled by the PoolChunks that are contained in this PoolChunkList.
return false;
}
for (PoolChunk<T> cur = head;;) {
long handle = cur.allocate(normCapacity); //取得哪个坐标下的某个值
if (handle < 0) { //在poolchunk中没有找到能装得下的,那么继续找下一个
cur = cur.next;
if (cur == null) {
return false;
}
} else {
cur.initBuf(buf, handle, reqCapacity);
if (cur.usage() >= maxUsage) {//chunked量用超了则移动向下一个链
remove(cur);
nextList.add(cur);
}
return true;
}
}
}
会轮循该链所有PoolChunk, 直到找到一个符合要求的内存块, 当分配完成后, 检查该PoolChunk是否因为使用率超过阈值需要放到别的队列中。
2. 若没有找到, 会去内存中申请一个PoolChunk的内存块, 在该PoolChunk中分配normCapacity大小的内存, 参考见Netty PoolChunk原理探究
3. 对PoolChunk进行初始化, 并将该PoolChunk加入qInit的链中。
这里有一个细节需要了解下, q050、q025、q000、qInit、q075按照这个顺序排序, 也就是说当在这几个对象都有可分配的内存时, 优先从 q050中分配, 最后从q075中分配。这样安排的考虑是:
- 将PoolChunk分配维持在较高的比例上。
- 保存一些空闲度比较大的内存, 以便大内存的分配。
总结
非内存池化的内存分配没有什么好说的, 并没有组织成什么结构来分配, 内存的释放主要由PoolChunk和PoolSubpage来释放。 本文主要讲了从poolArena上层结构tinySubpagePools、mallSubpagePools、050、q025、q000、qInit、q075分配内存、 大致的步骤, 至于从每个对象具体如何分配内存, 请看相关文章Netty PoolChunk原理探究、Netty-PoolSubpage原理探究.