在分析源码之前,我们先来了解一下Netty的内存管理机制。我们知道,jvm是自动管理内存的,这带来了一些好处,在分配内存的时候可以方便管理,也带来了一些问题。jvm每次分配内存的时候,都是先要去堆上申请内存空间进行分配,这就带来了很大的性能上的开销。当然,也可以使用堆外内存,Netty就用了堆外内存,但是内存的申请和释放,依然需要性能的开销。所以Netty实现了内存池来管理内存的申请和使用,提高了内存使用的效率。
PoolChunk就是Netty的内存管理的一种实现。Netty一次向系统申请16M的连续内存空间,这块内存通过PoolChunk对象包装,为了更细粒度的管理它,进一步的把这16M内存分成了2048个页(pageSize=8k)。页作为Netty内存管理的最基本的单位 ,所有的内存分配首先必须申请一块空闲页。Ps: 这里可能有一个疑问,如果申请1Byte的空间就分配一个页是不是太浪费空间,在Netty中Page还会被细化用于专门处理小于4096Byte的空间申请 那么这些Page需要通过某种数据结构跟算法管理起来。
先来看看PoolChunk有哪些属性
/**
* 所属 Arena 对象
*/
final PoolArena<T> arena;
/**
* 内存空间。
*
* @see PooledByteBuf#memory
*/
final T memory;
/**
* 是否非池化
*
* @see #PoolChunk(PoolArena, Object, int, int) 非池化。当申请的内存大小为 Huge 类型时,创建一整块 Chunk ,并且不拆分成若干 Page
* @see #PoolChunk(PoolArena, Object, int, int, int, int, int) 池化
*/
final boolean unpooled;
final int offset;
/**
* 分配信息满二叉树
*
* index 为节点编号
*/
private final byte[] memoryMap;
/**
* 高度信息满二叉树
*
* index 为节点编号
*/
private final byte[] depthMap;
/**
* PoolSubpage 数组
*/
private final PoolSubpage<T>[] subpages;
/**
* 判断分配请求内存是否为 Tiny/Small ,即分配 Subpage 内存块。
*
* Used to determine if the requested capacity is equal to or greater than pageSize.
*/
private final int subpageOverflowMask;
/**
* Page 大小,默认 8KB = 8192B
*/
private final int pageSize;
/**
* 从 1 开始左移到 {@link #pageSize} 的位数。默认 13 ,1 << 13 = 8192 。
*
* 具体用途,见 {@link #allocateRun(int)} 方法,计算指定容量所在满二叉树的层级。
*/
private final int pageShifts;
/**
* 满二叉树的高度。默认为 11 。
*/
private final int maxOrder;
/**
* Chunk 内存块占用大小。默认为 16M = 16 * 1024 。
*/
private final int chunkSize;
/**
* log2 {@link #chunkSize} 的结果。默认为 log2( 16M ) = 24 。
*/
private final int log2ChunkSize;
/**
* 可分配 {@link #subpages} 的数量,即数组大小。默认为 1 << maxOrder = 1 << 11 = 2048 。
*/
private final int maxSubpageAllocs;
Netty采用完全二叉树进行管理,树中每个叶子节点表示一个Page,即树高为12,中间节点表示页节点的持有者。有了上面的数据结构,那么页的申请跟释放就非常简单了,只需要从根节点一路遍历找到可用的节点即可。主要来看看PoolChunk是怎么分配内存的。
long allocate(int normCapacity) {
// 大于等于 Page 大小,分配 Page 内存块
if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
return allocateRun(normCapacity);
// 小于 Page 大小,分配 Subpage 内存块
} else {
return allocateSubpage(normCapacity);
}
}
private long allocateRun(int normCapacity) {
// 获得层级
int d = maxOrder - (log2(normCapacity) - pageShifts);
// 获得节点
int id = allocateNode(d);
// 未获得到节点,直接返回
if (id < 0) {
return id;
}
// 减少剩余可用字节数
freeBytes -= runLength(id);
return id;
}
private long allocateSubpage(int normCapacity) {
// 获得对应内存规格的 Subpage 双向链表的 head 节点
// Obtain the head of the PoolSubPage pool that is owned by the PoolArena and synchronize on it.
// This is need as we may add it back and so alter the linked-list structure.
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
// 加锁,分配过程会修改双向链表的结构,会存在多线程的情况。
synchronized (head) {
// 获得最底层的一个节点。Subpage 只能使用二叉树的最底层的节点。
int d = maxOrder; // subpages are only be allocated from pages i.e., leaves
int id = allocateNode(d);
// 获取失败,直接返回
if (id < 0) {
return id;
}
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;
// 减少剩余可用字节数
freeBytes -= pageSize;
// 获得节点对应的 subpages 数组的编号
int subpageIdx = subpageIdx(id);
// 获得节点对应的 subpages 数组的 PoolSubpage 对象
PoolSubpage<T> subpage = subpages[subpageIdx];
// 初始化 PoolSubpage 对象
if (subpage == null) { // 不存在,则进行创建 PoolSubpage 对象
subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
subpages[subpageIdx] = subpage;
} else { // 存在,则重新初始化 PoolSubpage 对象
subpage.init(head, normCapacity);
}
// 分配 PoolSubpage 内存块
return subpage.allocate();
}
}
Netty的内存按大小分为tiny,small,normal,而类型上可以分为PoolChunk,PoolSubpage,小于4096大小的内存就被分成PoolSubpage。Netty就是这样实现了对内存的管理。
PoolChunk就分析到这里了。
网友评论