Netty源码分析(七) PoolChunk

在分析源码之前,我们先来了解一下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 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[] 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 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[] subpages = this.subpages;
        final int pageSize = this.pageSize;

        // 减少剩余可用字节数
        freeBytes -= pageSize;

        // 获得节点对应的 subpages 数组的编号
        int subpageIdx = subpageIdx(id);
        // 获得节点对应的 subpages 数组的 PoolSubpage 对象
        PoolSubpage subpage = subpages[subpageIdx];
        // 初始化 PoolSubpage 对象
        if (subpage == null) { // 不存在,则进行创建 PoolSubpage 对象
            subpage = new PoolSubpage(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就分析到这里了。

你可能感兴趣的:(Netty源码分析(七) PoolChunk)