瓜农老梁

一个想分享点干货的家伙,微信公众号「瓜农老梁」

0%

Netty12# 池化内存框架流程

前言

本文简要梳理为什么使用池化内存?Netty使用池化内存从哪些方面提升了效率?梳理了池化内存的核心组件大体含义以及内存分配流程,勾勒池化内存的整体框架。后面文章会详细拆解每个点是如何实现的。

使用池化内存

为啥要使用池化内存呢? 主要以下两点:

1.频繁申请释放堆外直接内存耗时严重影响效率

2.减少小而不连续的空闲内存(也就是内存碎片)

Netty中又是如何体现内存池并提升效率的呢?

1.将申请的大块内存划分为不同的尺寸

2.不同尺寸的内存使用不同的分配算法,例如:Netty参考slab内存分配算法和Buddy(伙伴)分配算法

3.将划分的不同尺寸缓存起来,使用的时候先从缓存中获取

4.每个线程绑定了专属逻辑内存区域(PoolArena),减少资源竞争

5.使用对象池减少频繁创建销毁性能损耗(ByteBuf对象池)

6.内存用完后,按照特定算法重新合并到大块内存中,看起来像是内存池

内存池核心组件

内存池尺寸划分

Netty内存池划分了四种类型尺寸,Netty以Chunk为单位申请内存。 内存池主要指16M(默认)以下的内存,大于16M的内存分配不做缓存。

名称 范围
tiny 0~512Byte,内存分配参考了slab算法
small 512Byte~8KB,内存分配同tiny参考了slab算法
normal 8KB~16M,内存分配参考了
huge 大于16M

内存池核心类

类名 说明
PooledByteBufAllocator 内存池门面类,池化内存分配入口
PoolArena 逻辑上的一块内存区域,管理多个PoolChunk
PoolChunk 连续的内存区域,一个Chunk大小为16M。每个Chunk由Page组成,每个Page大小为8KB;包含nomal类型核心内存分配算法(参考了Buddy(伙伴)分配算法)
PoolSubpage 包含8KB以下tiny和small的核心分配算法(参考了slab分配算法)
PoolThreadCache 每个线程都有独立的PoolThreadCache,缓存了tiny类型、small类型、normal类型;分配时先从缓存中获取
Recycler 轻量级对象缓存池,避免频繁创建和消费性能损耗
ResourceLeakDetector 负责内存泄漏检测

内存分配流程

下面通过PooledByteBufAllocator#newDirectBuffer()方法,梳理内存分配的整体流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 @Override
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
PoolThreadCache cache = threadCache.get(); // 注解@1
PoolArena<ByteBuffer> directArena = cache.directArena;

final ByteBuf buf;
if (directArena != null) {
buf = directArena.allocate(cache, initialCapacity, maxCapacity); // 注解@2
} else {
buf = PlatformDependent.hasUnsafe() ?
UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
}
return toLeakAwareBuffer(buf); // 注解@3
}

注解@1 从当前线程中获取PoolThreadCache,也就是每个线程都绑定了PoolThreadCache

注解@2 执行内存分配过程

注解@3 通过ResourceLeakDetector检测内存泄漏

跟踪第二步,查看内存分配过程。

1
2
3
4
5
 PooledByteBuf<T> allocate(PoolThreadCache cache, int reqCapacity, int maxCapacity) {
PooledByteBuf<T> buf = newByteBuf(maxCapacity);
allocate(cache, buf, reqCapacity);
return buf;
}

PooledByteBuf从RECYCLER中获取(对象池)

1
2
3
4
5
static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
PooledUnsafeDirectByteBuf buf = RECYCLER.get();
buf.reuse(maxCapacity);
return buf;
}

下面的内存分配过程,先关注主干代码框架,分配过程整体包含了三个部分: tiny&small、small、huge。

huge:大于16M,直接分配堆外直接内存。

small:先从缓存中分配,缓存没有再从内存池分配(借鉴了buddy伙伴算法)

tiny&small:先从缓存中分配,缓存没有再从内存池分配(借鉴了slab算法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
final int normCapacity = normalizeCapacity(reqCapacity);
if (isTinyOrSmall(normCapacity)) { // capacity < pageSize(8KB) tiny或者small粒度
int tableIdx;
PoolSubpage<T>[] table;
boolean tiny = isTiny(normCapacity);
if (tiny) { // < 512 tiny粒度
if (cache.allocateTiny(this, buf, reqCapacity, normCapacity)) { // 缓存分配
return;
}
tableIdx = tinyIdx(normCapacity);
table = tinySubpagePools;
} else { // small 粒度
if (cache.allocateSmall(this, buf, reqCapacity, normCapacity)) {
return;
}
tableIdx = smallIdx(normCapacity);
table = smallSubpagePools;
}

final PoolSubpage<T> head = table[tableIdx]; // 获取对应的节点

synchronized (head) {
final PoolSubpage<T> s = head.next;
if (s != head) {
assert s.doNotDestroy && s.elemSize == normCapacity;
long handle = s.allocate();
assert handle >= 0;
s.chunk.initBufWithSubpage(buf, null, handle, reqCapacity);
incTinySmallAllocation(tiny);
return;
}
}
synchronized (this) {
allocateNormal(buf, reqCapacity, normCapacity);
}

incTinySmallAllocation(tiny);
return;
}
if (normCapacity <= chunkSize) { // Normal粒度(8K~16M)
if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) { // 尝试先从缓存分配
return;
}
synchronized (this) {
allocateNormal(buf, reqCapacity, normCapacity);
++allocationsNormal;
}
} else {
allocateHuge(buf, reqCapacity); // 大于16M的huge内存分配
}
}

小结下内存分配的整体过程

1.从RECYCLER对象池中复用PooledByteBuf

2.每个线程绑定了缓存PoolThreadCache

3.内存分配时,先从当前线程绑定的PoolThreadCache缓存分配;缓存没有再内存池分配,不同内存尺寸使用不同的分配算法

4.每个分配的Buffer都会由ResourceLeakDetector检测内存泄漏