Netty内存管理深度解析(下)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/TheLudlows/article/details/86481845

概述

1. PoolChunk

1.1 Buddy算法
1.2 PoolChunk初始化
1.3 分配

2. PoolSubPage

3. 分配和释放

上面的内容见上文:Netty内存管理深度解析(上)

4. PoolChunkList

Netty内存管理概述中讲到过Arena将内存分为很多Chunk进行管理,其实就是通过多个PoolChunkList来保存,并且根据Chunk的使用率,动态的移动至对应的ChunkList中。在Arena会初始化六种PoolChunklist分别为:QINIT,Q0,Q25,Q50,Q75,Q100。
Netty
它们之间除了QINIT外形成双向链表;PoolChunkList中的Chunk块也形成双向链表,其中头结点是双向链表的尾部,且新加入的节点也加到尾部。
Chunk随着内存使用率的变化,会在PoolChunkList中移动,初始时都在QINI,随着使用率增大,移动到Q0,Q25等;随着使用率降低,又移回Q0,当Q0中的Chunk块不再使用时,从Q0中移除。

PoolChunkList的属性如下:

final class PoolChunkList<T> implements PoolChunkListMetric {
    private final PoolArena<T> arena;// 所属的Arena
    private final int minUsage;// 最小内存使用率
    private final int maxUsage;// 最大内存使用率
    private final int maxCapacity;// 下的一个Chunk可分配的最大字节数
    private PoolChunk<T> head;// head节点
    private PoolChunkList<T> prevList;// 上一个状态list
    private final PoolChunkList<T> nextList;// 下一个状态list
}

在PoolArena中会创建状态列表,截取部分代码:

q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);

q100.prevList(q075);
q075.prevList(q050);
q050.prevList(q025);
q025.prevList(q000);
q000.prevList(null);
qInit.prevList(qInit);

下面看它的构造方法PoolChunkList的构造方法:

PoolChunkList(PoolArena<T> arena, PoolChunkList<T> nextList, int minUsage, int maxUsage, int chunkSize) {
    this.arena = arena;
    this.nextList = nextList;
    this.minUsage = minUsage;
    this.maxUsage = maxUsage;
    maxCapacity = calculateMaxCapacity(minUsage, chunkSize);
}
private static int calculateMaxCapacity(int minUsage, int chunkSize) {
    minUsage = minUsage0(minUsage);
    if (minUsage == 100) return 0;// Q100 不能再分配
    // 比如Q25中可以分配的最大内存为0.75 * ChunkSize
    return  (int) (chunkSize * (100L - minUsage) / 100L);
}

PoolArena负责管理QInit~Q100,其内部的Chunk的管理是在PoolChunkList内部,我们看下添加Chunk的逻辑

void add(PoolChunk<T> chunk) {
    if (chunk.usage() >= maxUsage) { // chunk的使用率如果大于当前list的最大使用率,调用下一个list的add方法
        nextList.add(chunk);
        return;
    }
    add0(chunk); // 添加至内部chunk列表头部
}

接下来分析关键的分配过程,代码如下:

boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    // 根据head是否为null来判断此PoolChunkList没有chunk可用,如果没有,则返回false。
    if (head == null || normCapacity > maxCapacity) {
        return false;
    }
    // 遍chunk列表
    for (PoolChunk<T> cur = head;;) {
        // PoolChunk.allocate见上文
        long handle = cur.allocate(normCapacity);
        if (handle < 0) {
            cur = cur.next;
            if (cur == null) {
                return false;
            }
        } else {
            // 找到满足的chunk
            cur.initBuf(buf, handle, reqCapacity);
            if (cur.usage() >= maxUsage) { // 在此计算使用率,如果有必要则向后移动
                remove(cur);
                nextList.add(cur);
            }
            return true;
        }
    }
}

ChunkList内容比较易懂,部分代码就不在此列出,下面总结一下

  1. 每个PoolChunkList中用head字段维护一个PoolChunk链表的头部
  2. 当需要进行内存分配时,会依次遍历该PoolChunkList中的PoolChunk节点来完成,完成之后,会判断此PoolChunk的内存使用量是否大于该PoolChunkLis他的maxUsage,如果大于,则将此chunk放到下一个PoolChunkList中。
  3. 当chunk由于内存释放的原因而导致内存使用量减少,即剩余内存量增大,如果小于此PoolChunkList的minUsage,则将其加入到上一个PoolChunkList中去。

5. PoolArena

终于到了这一节了,前面所有的内容都是对PoolArena的铺垫,比如PoolChunk、PoolSubPage相当于零件,PoolArena是对这些零件的组装,提供内存管理的功能。
为了缓解线程竞争,一般通过创建多个poolArena细化锁的粒度,提高并发执行的效率。

PoolArena类是一个抽象类,这是因为ByteBuf分为Heap和Direct,所以PoolArena同样分为:Heap和Direct。实现类为PoolArena的内部类。下面先从PoolArena的属性看起

final PooledByteBufAllocator parent;// 表示该PoolArena的allocator
private final int maxOrder; ;// 表示chunk中由Page节点构成的二叉树的最大高度。默认11
final int pageSize;// page的大小,默认8K
final int pageShifts;// pageShifts=log(pageSize),默认13
final int chunkSize;;// chunk的大小,默认16M
final int subpageOverflowMask;
static final int numTinySubpagePools = 512 >>> 4;// 用来分配tiny内存的数组长度
final int numSmallSubpagePools; //用来分配small内存的数组长度
//tinySubpagePools来缓存(或说是存储)用来分配tiny(小于512)内存的Page;
//smallSubpagePools来缓存用来分配small(大于等于512且小于pageSize)内存的Page
private final PoolSubpage<T>[] tinySubpagePools;
private final PoolSubpage<T>[] smallSubpagePools;
// PoolChunkList作为容器存放相同状态的Chunk块,相关变量如下:
private final PoolChunkList<T> q050;
private final PoolChunkList<T> q025;
private final PoolChunkList<T> q000;
private final PoolChunkList<T> qInit;
private final PoolChunkList<T> q075;
private final PoolChunkList<T> q100;
private final List<PoolChunkListMetric> chunkListMetrics;
private long allocationsNormal;

继续看构造方法:

protected PoolArena(PooledByteBufAllocator parent, int pageSize,
      int maxOrder, int pageShifts, int chunkSize, int cacheAlignment) {
    this.parent = parent;
    this.pageSize = pageSize;
    this.maxOrder = maxOrder;
    this.pageShifts = pageShifts;
    this.chunkSize = chunkSize;
    directMemoryCacheAlignment = cacheAlignment;
    directMemoryCacheAlignmentMask = cacheAlignment - 1;
    subpageOverflowMask = ~(pageSize - 1);
    tinySubpagePools = newSubpagePoolArray(numTinySubpagePools);
    for (int i = 0; i < tinySubpagePools.length; i ++) {
        tinySubpagePools[i] = newSubpagePoolHead(pageSize);
    }

    numSmallSubpagePools = pageShifts - 9; //该变量用于判断申请的内存大小与page之间的关系,是大于,还是小于
    smallSubpagePools = newSubpagePoolArray(numSmallSubpagePools);
    for (int i = 0; i < smallSubpagePools.length; i ++) {
        smallSubpagePools[i] = newSubpagePoolHead(pageSize);
    }
    q100 = new PoolChunkList<T>(this, null, 100, Integer.MAX_VALUE, chunkSize);
    q075 = new PoolChunkList<T>(this, q100, 75, 100, chunkSize);
    q050 = new PoolChunkList<T>(this, q075, 50, 100, chunkSize);
    q025 = new PoolChunkList<T>(this, q050, 25, 75, chunkSize);
    q000 = new PoolChunkList<T>(this, q025, 1, 50, chunkSize);
    qInit = new PoolChunkList<T>(this, q000, Integer.MIN_VALUE, 25, chunkSize);

    q100.prevList(q075);
    q075.prevList(q050);
    q050.prevList(q025);
    q025.prevList(q000);
    q000.prevList(null);
    qInit.prevList(qInit);

    List<PoolChunkListMetric> metrics = new ArrayList<PoolChunkListMetric>(6);
    metrics.add(qInit);
    metrics.add(q000);
    metrics.add(q025);
    metrics.add(q050);
    metrics.add(q075);
    metrics.add(q100);
    chunkListMetrics = Collections.unmodifiableList(metrics);
}

总的来看主要是对PoolChunkList和SubpagePools进行初始工作,其中前者管理的Chunk,用于分配大于PageSize的内存,后者哟用于管理SubPage,用于分配小于PageSize的内存。
SubpagePools数组中只保存头结点,它是一个空的节点。SubpagePools分为两类:

  • tinySubpagePools:用于分配小于512字节的内存,默认长度为32,因为内存分配最小为16,每次增加16,直到512,区间[16,512)一共有32个不同值
  • smallSubpagePools:用于分配大于等于512字节的内存,默认长度为4

poolChunkList用于分配大于8k的内存;

  • qInit:存储内存利用率0-25%的chunk
  • q000:存储内存利用率1-50%的chunk
  • q025:存储内存利用率25-75%的chunk
  • q050:存储内存利用率50-100%的chunk
  • q075:存储内存利用率75-100%的chunk
  • q100:存储内存利用率100%的chunk

有一个疑问,初始化SubpagePools时newSubpagePoolHead创建额PoolSubPage是一样的,那么如何区分每个元素之间的代表不同大小的内存?
答案就在下面代码中

PoolSubpage<T> findSubpagePoolHead(int elemSize) {
    int tableIdx;
    PoolSubpage<T>[] table;
    if (isTiny(elemSize)) { // < 512
        tableIdx = elemSize >>> 4;
        table = tinySubpagePools;
    } else {
        tableIdx = 0;
        elemSize >>>= 10;
        while (elemSize != 0) {
            elemSize >>>= 1;
            tableIdx ++;
        }
        table = smallSubpagePools;
    }

    return table[tableIdx];
}
  1. 当elemSize小于512时,块大小为elemSize的page将存储在tinySubpagePools[elemSize>>>4]的位置上,用于之后分配大小为elemSize(小于512的tiny内存)的内存请求。也就是说:tinySubpagePools[tableIdx]处的page负责分配大小为16*tableIdx
  2. 当elemSize在区间[512,pageSize)范围内时,块大小为elemSize的page将存储在tinySubpagePools[{(log(elemSize)-10)+1}]的位置上,用于之后分配大小为elemSize的内存请求。
    也就是说:smallSubpagePools[0]处的page负责分配大小为512的块内存,smallSubpagePools[1]处的page负责分配大小为1024的内存,smallSubpagePools[2]处的page负责分配大小为2048的内存,按这种倍增的方式依次类推。

接下去看看poolArena最核心的内存分配,实现如下:.

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

此方法是PooledByteBufAllocator分配的入口,先是创建一个PooledByteBuf的实例,跟进入发现newByteBuf方法是一个抽象方法protected abstract PooledByteBuf<T> newByteBuf(int maxCapacity),前面提到过,PoolArena子类是HeadArena和DirectArena,分别是对直接内存和堆内存的实现。
一堆内存为例:

@Override
protected PooledByteBuf<byte[]> newByteBuf(int maxCapacity) {
      return HAS_UNSAFE ? PooledUnsafeHeapByteBuf.newUnsafeInstance(maxCapacity)
              : PooledHeapByteBuf.newInstance(maxCapacity);
}

根据平台是否支持Unsafe,通过不同的途径创建,继续跟进:

static PooledHeapByteBuf newInstance(int maxCapacity) {
    PooledHeapByteBuf buf = RECYCLER.get();
    buf.reuse(maxCapacity);
    return buf;
}

这里有比较有意思了,通过Recycle对象池获取PooledHeapByteBuf ,Recycle的具体细节我们后面讨论,总之处处可见Netty对于性能的极致追求。
当创建好了ByteBuf之后,需要将ByteBuf对象和实际内存关联:

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
    final int normCapacity = normalizeCapacity(reqCapacity);// 规范容量,将reqCapacity变为2的幂次方
    if (isTinyOrSmall(normCapacity)) { // capacity < pageSize
        int tableIdx;
        PoolSubpage<T>[] table;
        boolean tiny = isTiny(normCapacity);
        if (tiny) { // < 512
            if (cache.allocateTiny(this, buf, reqCapacity, normCapacity))
                return;// 尝试从ThreadCache进行分配
            tableIdx = tinyIdx(normCapacity);
            table = tinySubpagePools;
        } else { // >= 512
            if (cache.allocateSmall(this, buf, reqCapacity, normCapacity))
                return;
            tableIdx = smallIdx(normCapacity);
            table = smallSubpagePools;
        }
        // 得到Subpage双向链表的头结点
        final PoolSubpage<T> head = table[tableIdx];
        synchronized (head) {
            final PoolSubpage<T> s = head.next;
            if (s != head) { // 如果Subpage双向列表不为空,第一次分配一定为空
                long handle = s.allocate();
                s.chunk.initBufWithSubpage(buf, handle, reqCapacity);
                incTinySmallAllocation(tiny);
                return;
            }
        }
        synchronized (this) { // // 双向循环链表还没初始化,使用normal分配
            allocateNormal(buf, reqCapacity, normCapacity);
        }

        incTinySmallAllocation(tiny);
        return;
    }
    if (normCapacity <= chunkSize) { // pageSize <= capacity <= chunkSize
        if (cache.allocateNormal(this, buf, reqCapacity, normCapacity))
            return;
        synchronized (this) {
            allocateNormal(buf, reqCapacity, normCapacity);
            ++allocationsNormal;
        }
    } else {// capacity >= chunkSize
        allocateHuge(buf, reqCapacity);
    }
}
  1. 分配内存有四种情况 0512,512pageSize,PageSize~ChunkSize,大于ChunkSize,除了分配大于大于ChunkSize的内存其他情况都是先依靠ThreadCache中分配,
    主要是为了消除了多线程竞争,提高内存分配效率,当上一次分配的内存使用完并释放时,会将其加入到poolThreadCache中,提供该线程下次申请时使用。这部分内容会在下一届讲述。
  2. 如果是分配小内存,则尝试从tinySubpagePools或smallSubpagePools中分配内存,如果没有合适subpage,则采用方法allocateNormal分配内存
  3. 如果分配一个page以上的内存,直接采用方法allocateNormal分配内存。
  4. 如果分配ChunkSize以上的内存,直接采用方法allocateHuge分配内存。
    下面看allocateNormal的实现:
private void allocateNormal(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
    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)) {
        return;
    }

    // 无Chunk或已存Chunk不能满足分配,新增一个Chunk
    PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
    long handle = c.allocate(normCapacity);
    assert handle > 0;
    c.initBuf(buf, handle, reqCapacity);
    qInit.add(c);
}

先从ChunkList中分配,当然第一次分配ChunkList中是没有Chunk的,继续往下新建一个Chunk,通过Chunk分配(Chunk分配的分析在上篇文章中已经详细阐述),然后调用Chunk.initBuf方法,此方法在上文中也解释过,最后将Chunk添加至qInit 列表中。

6. 缓存

Jemalloc的另一个重要的概念是本地缓冲Thread-Local Storage,将释放后的内存使用信息保存在线程中以提高内存分配效率。
在Netty中,担负TLS的类有:

  • PoolThreadLocalCache 类似ThreadLocal对象,内部保存线程本地缓存
  • PoolThreadCache 缓冲池,每个线程一个实例,保存回收的内存信息(ThreadLocal中的value)
  • MemoryRegionCache 内部有一个队列,保存了内存释放时的数据Chunk和Handle
  • Recycler 一个轻量级对象池
6.1 PoolThreadLocalCache

PoolThreadLocalCache继承FastThreadLocal对象,FastThreadLocal是netty自己实现的ThreadLocal机制,详情见 Netty进阶:自顶向下解析FastThreadLocal

每个线程保存了PoolThreadCache对象,使用get时,如果ThreadLocal内部没有则会调用initialValue()方法创建。PoolThreadCache创建过程中会选择内存使用最少的Arena来创建PoolThreadCache。

final class PoolThreadLocalCache extends FastThreadLocal<PoolThreadCache> {
    private final boolean useCacheForAllThreads;
    PoolThreadLocalCache(boolean useCacheForAllThreads) {
        this.useCacheForAllThreads = useCacheForAllThreads;
    }

    @Override
    protected synchronized PoolThreadCache initialValue() {
        final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas);
        final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas);

        Thread current = Thread.currentThread();
        if (useCacheForAllThreads || current instanceof FastThreadLocalThread) {
            return new PoolThreadCache(
                    heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize,
                    DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL);
        }
        return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0);
    }
    @Override
    protected void onRemoval(PoolThreadCache threadCache) {
        threadCache.free();
    }
    private <T> PoolArena<T> leastUsedArena(PoolArena<T>[] arenas) {
        PoolArena<T> minArena = arenas[0];
        for (int i = 1; i < arenas.length; i++) {
            PoolArena<T> arena = arenas[i];
            if (arena.numThreadCaches.get() < minArena.numThreadCaches.get()) {
                minArena = arena;
            }
        }
        return minArena;
    }
}
6.2 PoolThreadCache

PoolThreadCache记录了线程本地保存的内存池,分配的ByteBuf释放时会被保存到该对象的实例中。PoolThreadCache内部保存了tiny/small/normal的堆内存和直接内存的MemoryRegionCache数组

final PoolArena<byte[]> heapArena; // 堆Arena
final PoolArena<ByteBuffer> directArena; // 直接内存Arena
private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;// tiny-heap
private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;// small-heap
private final MemoryRegionCache<byte[]>[] normalHeapCaches;// normal-heap
private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;// tiny-direct
private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;// small-direct
private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;// normal-direct

总结

# 内存分配

netty进行分配时,主要流程比较简单,首先从对象池获取ByteBuf,之后从线程本地缓存MemoryRegionCache中查找内存页,再从Arena的内存池中查找,最后查找Chunk,分配SubPage,最后初始化bytebuf。

  1. 从线程的本地缓存中获取PoolThreadCache对象,如果没有,则选择使用空间最少的Arena创建PoolThreadCache实例并保存至线程本地

  2. 使用PoolThreadCache的Arena从对象池中获取ByteBuf,对象池中默认没有释放的对象,会创建新对象。Arena根据HAS_UNSAFE判断是PooledUnsafeHeapByteBuf还是PooledHeapByteBuf进行相应处理。

  3. 根据请求的内存大小,判断其规格:tiny/small/normal/huge

    3.1 若大小为tiny(0,512),从本地缓存的PoolThreadCache的MemoryRegionCache中查看是否有释放后的内存可以重用,若有则初始化PooledByteBuf;本地缓存池中没有可重用内存,先根据大小定位到在Arena的tinySubpagePools的位置idx,然后在其中查找可用的PoolSubpage,如果找到内存页,则使用内存页的chunk的初始化PoolSubpage。

    3.2 若大小为small[512,pagesize],逻辑与tiny类似

    3.3 tiny和small在MemoryRegionCache和tinySubpagePools/smallSubpagePools中未找到可用分配的内存页则会调用allocateNormal寻找chunk

    3.4 若大小为normal(pagesize,chunksize],会先从MemoryRegionCache中查找可用的回收后的缓存,如果未找到则会调用allocateNormal寻找chunk

  4. 寻找chunk时,先从q050->q025->q000->qInit->q075查找可用的chunk,如果没有找到,会创建PoolChunk对象的实例,创建chunk时会分配实际内存(heap使用byte[],direct使用ByteBuffer.allocateDirect)。找到chunk后,在chunk中查找或创建内存页,最后返回一个Handle。Handle是一个long型整数,记录了chunk和内部的偏移。

  5. chunk寻找内存页的过程:根据请求大小计算idx,并找到tinySubpagePools或smallSubpagePools中idx位置的链表head;再根据大小计算在chunk的memoryMap叶子节点的index,如果chunk的subpages数组中index位置为空,说明没有创建PoolSubpage,创建新的内存页之后,并将其加入到chunk的subpages数组。最后修改subpage中的bitmap,讲该内存页加入到tinySubpagePools或smallSubpagePools对应位置的链表中。最后计算出分配出的内存的Handle,0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx; 内部保存了所分配的内存在chunk中的内存页位置memoryMapIdx和在内存页中的位置bitmapIdx

  6. 获得Handle之后,对第2步中获取的ByteBuf进行初始化。

  7. 如果chunk是新创建的,还需要加入到Arena的chunklist中。

# 内存释放

调用ByteBuf的release方法可以释放内存,主要分为两步:使用Arena释放ByteBuf,将ByteBuf回收到对象池中。

Arena释放ByteBuf时,如果线程本地PoolThreadCache不为空,查找PoolThreadCache的Caches数组中对应的MemoryRegionCache,将chunk和Handle加入到MemoryRegionCache的queue中。

展开阅读全文

没有更多推荐了,返回首页