9. Netty中内存分配和回收,PooledByteBufAllocator,PoolArena,PoolThreadCache,PoolChunk,PoolSubpage

当客户端建立连接后,客户端向服务端发送消息,服务端需要读取消息,一般在常见的在worker线程中会有如下操作:

NioByteUnsafe

AbstractNioByteChannel 进行实际read

        public final void read() {
            final ChannelConfig config = config();
            if (shouldBreakReadReady(config)) {
                clearReadPending();
                return;
            }
            final ChannelPipeline pipeline = pipeline();

            //对应 DefaultChannelConfig中 ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
            //不同平台和不同环境有不同设置
            final ByteBufAllocator allocator = config.getAllocator();
            // channelConfig中 rcvBufAllocator 为 AdaptiveRecvByteBufAllocator,
            // 继承DefaultMaxMessagesRecvByteBufAllocator 能够设置每次最大读取数据大小
            // AdaptiveRecvByteBufAllocator HandleImpl
            final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
            allocHandle.reset(config);
            ByteBuf byteBuf = null;
            boolean close = false;
            try {
                do {
                    byteBuf = allocHandle.allocate(allocator);
                    allocHandle.lastBytesRead(doReadBytes(byteBuf));
                    if (allocHandle.lastBytesRead() <= 0) {
                        byteBuf.release();
                        byteBuf = null;
                        close = allocHandle.lastBytesRead() < 0;
                        if (close) {
                            readPending = false;
                        }
                        break;
                    }
                    allocHandle.incMessagesRead(1);
                    readPending = false;
                    pipeline.fireChannelRead(byteBuf);
                    byteBuf = null;
                } while (allocHandle.continueReading());
                allocHandle.readComplete();
                pipeline.fireChannelReadComplete();
                if (close) {
                    closeOnRead(pipeline);
                }
            } catch (Throwable t) {
                handleReadException(pipeline, byteBuf, t, close, allocHandle);
            } finally {channelReadComplete(...) method
                if (!readPending && !config.isAutoRead()) {
                    removeReadOp();
                }
            }
        }
    }

ByteBufUtil中进行实际的初始化,在其静态代码块中实现:

 static {
        String allocType = SystemPropertyUtil.get(
                "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
        allocType = allocType.toLowerCase(Locale.US).trim();
        ByteBufAllocator alloc;
        if ("unpooled".equals(allocType)) {
            alloc = UnpooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else if ("pooled".equals(allocType)) {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
        }
        DEFAULT_ALLOCATOR = alloc;
        THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 0);
        logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);
        MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024);
     
    }

我们这里以PooledByteBufAllocator分配堆内存为例说明为例进行说明。
当进行内存分配allocate时候,实际上是调用了内存分配器进行分配,
这里可以通过系统变量io.netty.allocator.type来指定内存分配类型,取值unpooled pooled,如果没有指定的话且不是安卓平台,则返回的是pooled
allocHandle.allocate(allocator);调用的是Allocatetor.ioBuffer进行操作,这里首先判断是否能够支持对外内存分配,如果能够支持对外直接内存分配,则优先使用对外直接内存

public ByteBuf ioBuffer(int initialCapacity) {
        if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
            return directBuffer(initialCapacity);
        }
        return heapBuffer(initialCapacity);
    }
// PooledByteBufAllocator.java
  protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        PoolThreadCache cache = threadCache.get();
        PoolArena<ByteBuffer> directArena = cache.directArena;

        final ByteBuf buf;
        if (directArena != null) {
            buf = directArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            buf = PlatformDependent.hasUnsafe() ?
                    UnsafeByteBufUtil.newUnsafeDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }

        return toLeakAwareBuffer(buf);
    }

华丽的分割线


在这之前,先说下Netty中池化的处理,以PooledDirectByteBuf举例,其内部维护了一个对象池化处理器:

private static final ObjectPool<PooledDirectByteBuf> RECYCLER = ObjectPool.newPool(
            new ObjectCreator<PooledDirectByteBuf>() {
        @Override
        public PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) {
            return new PooledDirectByteBuf(handle, 0);
        }
    });

    
//ObjectPool.newPool定义如下:
 public static <T> ObjectPool<T> newPool(final ObjectCreator<T> creator) {
        return new RecyclerObjectPool<T>(ObjectUtil.checkNotNull(creator, "creator"));
    }

每次如果要创建一个新的PooledByteBuf时,通过:

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

其中RECYCLER.get();调用对应的是在ObjectPool中定义的RecyclerObjectPoolRecycler<T> recycler;循环回收器

private static final class RecyclerObjectPool<T> extends ObjectPool<T> {
        private final Recycler<T> recycler;
        RecyclerObjectPool(final ObjectCreator<T> creator) {
             recycler = new Recycler<T>() {
                @Override
                protected T newObject(Handle<T> handle) {
                    return creator.newObject(handle);
                }
            };
        }
        @Override
        public T get() {
            return recycler.get();
        }
    }

Recylerget定义如下:

 public final T get() {
        if (maxCapacityPerThread == 0) { //当线程最大容量为0的时候,返回对象,但是不回收
            return newObject((Handle<T>) NOOP_HANDLE);
        }
        Stack<T> stack = threadLocal.get(); //线程本地变量获取线程对应的栈
        DefaultHandle<T> handle = stack.pop();
        if (handle == null) {
            handle = stack.newHandle();
            handle.value = newObject(handle); //没有可循环利用的对象的时候,调用对象创建器创建一个新的对象,并把循环处理器 handle一块传递过去
        }
        return (T) handle.value;
    }

stack.newHandler()返回的是默认带有循环利用的DefaultHandler:

 DefaultHandle<T> newHandle() {
            return new DefaultHandle<T>(this);
        }

当对象释放,可以循环回收的时候,调用Recyler.recyle进行回收:

public final boolean recycle(T o, Handle<T> handle) {
        if (handle == NOOP_HANDLE) {
            return false;
        }
        DefaultHandle<T> h = (DefaultHandle<T>) handle;
        if (h.stack.parent != this) {
            return false;
        }
        h.recycle(o);
        return true;
    }
//DefaultHandle中回收处理
 public void recycle(Object object) {
            if (object != value) {
                throw new IllegalArgumentException("object does not belong to handle");
            }
            Stack<?> stack = this.stack;
            if (lastRecycledId != recycleId || stack == null) {
                throw new IllegalStateException("recycled already");
            }
            stack.push(this);
        }

从上面代码可以发现,对象池里维护了一个Recyler回收器,回收器里面为每个线程维护了一个线程局部变量Stack栈结构,里面的对象是DefaultHandle对象,来进行实际的回收处理,当需要从对象池中获取对象时,
首先判断当前线程对象的Stack中能否获取,如果不能,则通过定义的ObjectCreator.newObject创建一个新的对象。
当对象需要进行回收时,先判断handle是否是需要回收,如果不是,则忽略。否则,将回收的对象放入当前线程本地变量的Stack中去,这样完成了对象池的循环利用。


进行内存分配的时候首先从当前线程本地缓存中获取PoolThreadCache,获取对应PoolArena,这里的threadCache 实现为PoolThreadLocalCache:

// PoolThreadLocalCache.java
protected synchronized PoolThreadCache initialValue() {
            final PoolArena<byte[]> heapArena = leastUsedArena(heapArenas);
            final PoolArena<ByteBuffer> directArena = leastUsedArena(directArenas);
            final Thread current = Thread.currentThread();
            if (useCacheForAllThreads || current instanceof FastThreadLocalThread) {
                final PoolThreadCache cache = new PoolThreadCache(
                        heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize,
                        DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL);
                if (DEFAULT_CACHE_TRIM_INTERVAL_MILLIS > 0) {
                    final EventExecutor executor = ThreadExecutorMap.currentExecutor();
                    if (executor != null) {
                        executor.scheduleAtFixedRate(trimTask, DEFAULT_CACHE_TRIM_INTERVAL_MILLIS,
                                DEFAULT_CACHE_TRIM_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
                    }
                }
                return cache;
            }
            return new PoolThreadCache(heapArena, directArena, 0, 0, 0, 0, 0);
        }
private <T> PoolArena<T> leastUsedArena(PoolArena<T>[] arenas) {
            if (arenas == null || arenas.length == 0) {
                return null;
            }
            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;
        }

这里会从PooledByteBufAllocatorarenas数组中选取一个空闲最大的去进行分配,这里的arenas数组大小取值如下:

final int defaultMinNumArena = NettyRuntime.availableProcessors() * 2;
        DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
                SystemPropertyUtil.getInt(
                        "io.netty.allocator.numDirectArenas",
                        (int) Math.min(
                                defaultMinNumArena,
                                PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));

可以看到这里默认情况下,取值应该是CPU*2,和前面说的LoopGroup的线程数一样,能够保证一个线程一个arena.
获取到PoolArena之后,然后会通过PoolArena进行内存的分配。

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

protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) {
            if (HAS_UNSAFE) {
                return PooledUnsafeDirectByteBuf.newInstance(maxCapacity);
            } else {
                return PooledDirectByteBuf.newInstance(maxCapacity);
            }
        }
static PooledUnsafeDirectByteBuf newInstance(int maxCapacity) {
        PooledUnsafeDirectByteBuf buf = RECYCLER.get();
        buf.reuse(maxCapacity);
        return buf;
    }

这里首先是获取一个PooledByteBuf实例,这个对象实例也是会进行池化处理,返回了一个PooledUnsafeDirectByteBuf
需要注意的是,这个对象是对堆外内存做了一次封装,它持有了一个堆外内存的内存地址,根据这个地址来进行内存的写入和读取
获取到一个PooledByteBuf之后,然后进行内存申请分配,将分配到的内存关联到PooledByteBuf上。

Netty中通过 PoolArena进行内存的管理分配:

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
        final int normCapacity = normalizeCapacity(reqCapacity);
        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;
                }
                tableIdx = tinyIdx(normCapacity);
                table = tinySubpagePools;
            } else {
                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) {
            if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                return;
            }
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
                ++allocationsNormal;
            }
        } else {
            allocateHuge(buf, reqCapacity);
        }
    }

这里首先一上来就会通过normalizeCapacity对待申请的内存空间大小进行规范化,我们来看下是怎么规范处理的:

int normalizeCapacity(int reqCapacity) {
        checkPositiveOrZero(reqCapacity, "reqCapacity");
        if (reqCapacity >= chunkSize) {
            return directMemoryCacheAlignment == 0 ? reqCapacity : alignCapacity(reqCapacity);
        }
        if (!isTiny(reqCapacity)) { // >= 512
            int normalizedCapacity = reqCapacity;
            normalizedCapacity --;
            normalizedCapacity |= normalizedCapacity >>>  1;
            normalizedCapacity |= normalizedCapacity >>>  2;
            normalizedCapacity |= normalizedCapacity >>>  4;
            normalizedCapacity |= normalizedCapacity >>>  8;
            normalizedCapacity |= normalizedCapacity >>> 16;
            normalizedCapacity ++;

            if (normalizedCapacity < 0) {
                normalizedCapacity >>>= 1;
            }
            assert directMemoryCacheAlignment == 0 || (normalizedCapacity & directMemoryCacheAlignmentMask) == 0;

            return normalizedCapacity;
        }
        // directMemoryCacheAlignment默认为0 
        if (directMemoryCacheAlignment > 0) {
            return alignCapacity(reqCapacity);
        }
        if ((reqCapacity & 15) == 0) {
            return reqCapacity;
        }

        return (reqCapacity & ~15) + 16;
    }

可以看到,如果是 <512,那么就看能否被16整除,如果能,直接返回,否则会向上取其16的倍数值,而如果>=512,则是会成倍扩充,即512 -> 1024 ->2048 -> 4096->8192这样扩充,到这里就将请求的内存大小进行了调整规范,在Netty中,按照如下规格对北村进行了划分:

Tiny < 512B
512B <= Small < 8KB (pageSize)
8KB <= Normal <= 16MB(chunkSize)
Huge > 16MB (不会进行池化缓存,分配完,直接回收)

这里首先看本地线程缓存是否能够分配,如果能够分配那么直接分配,这块我们回头说。
如果本地线程无法分配,那么会尝试在PoolArena的SubpagePools数组上分配,会根据调整后的请求内存大小,找到在数组中的下标,在PoolArena中有如下两个数组:

 private final PoolSubpage<T>[] tinySubpagePools;
 private final PoolSubpage<T>[] smallSubpagePools;

而我们前面分析,调整后的内存主要分为两种<512>=512&<8192,其中<512的规格都是按照16的倍数增长,而>=512&<8192则是512 -> 1024 ->2048 -> 4096->8192扩充,因此对于<512的规格,将normCapacity >>> 4就能够得到这个规格在tinySubpagePools的下标,而对于>=512&<8192则是先normCapacity>>>10,相当于除了1024,但是这里是成倍增加,所以如果不是0,还是需要除以2

static int tinyIdx(int normCapacity) {
        return normCapacity >>> 4;
    }
static int smallIdx(int normCapacity) {
        int tableIdx = 0;
        int i = normCapacity >>> 10;
        while (i != 0) {
            i >>>= 1;
            tableIdx ++;
        }
        return tableIdx;
    }

从上面的查找SubpagePools元素,我们发现,在PoolArena中tinySubpagePools和smallSubpagePools中每个元素都是代表了一个规格的内存,而PoolSubpage实际上是一个链表,这样PoolArena将同样规格的内存放在了PoolSubpage的一个链上,如果能够在这个PoolSubpage上无法分配,在PoolArena上维护的几个PoolChunkList上分配,如果还是无法分配,那么会生成一个新的PoolChunk上分配。
在Netty底层,内存的实际分配和管理是通过PoolChunk来实现的,PoolChunk申请到的内存,实际上一段连续的内存空间,这个内存被切分成了很多块,每块会有标志位表示是否已经被分配。每次获取指定大小的内存时,并不是申请多大就给多大,而是会进行大小标准化,这样会造成一些空间的浪费,但是这样方便管理维护。
PoolChunk中会将这连续的内存空间划分为多个page,默认情况下PoolChunk大小为16MB,每个page为8KB,PoolChunk会在其内部维护一颗平衡二叉树,
根节点2^(24-0B)=16MB,然后其子节点为 2^(24-1)B=8MB大小,这样,这个二叉树一共12层,最底层的叶子节点,每个节点的大小就是2^(24-11)B=8KB
这样,如果我们要获取一个8KB大小,直接在叶子节点找一个,并将其状态置为已经分配,如果想要一个4MB大小,那么直接在第2层找一个可用的节点,通过公式:
depth = 11-(log2(normCapacity)-13,我们能够很轻松找到需要的内存大小在这个二叉树的哪一层,找到这层后,其子节点需要设置为已经分配状态,父节点也要更新对应状态。
在PoolChunk中,有两个数组来维护这个信息:

private final byte[] memoryMap;
private final byte[] depthMap;

我们看下这几个参数是在Netty中是怎么获取的:

// PooledByteBufAllocator.java
int defaultPageSize = SystemPropertyUtil.getInt("io.netty.allocator.pageSize", 8192);
int defaultMaxOrder = SystemPropertyUtil.getInt("io.netty.allocator.maxOrder", 11);// 8192 << 11 = 16 MiB per chunk
DEFAULT_NUM_DIRECT_ARENA = Math.max(0,
                SystemPropertyUtil.getInt(
                        "io.netty.allocator.numDirectArenas",
                        (int) Math.min(
                                defaultMinNumArena,
                                PlatformDependent.maxDirectMemory() / defaultChunkSize / 2 / 3)));
DEFAULT_TINY_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.tinyCacheSize", 512);
DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.smallCacheSize", 256);
DEFAULT_NORMAL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.normalCacheSize", 64);

这里的memoryMap和depthMap初始逻辑如下:

maxSubpageAllocs = 1 << maxOrder;
memoryMap = new byte[maxSubpageAllocs << 1];
depthMap = new byte[memoryMap.length];
int memoryMapIndex = 1;
        for (int d = 0; d <= maxOrder; ++ d) { // move down the tree one level at a time
            int depth = 1 << d;
            for (int p = 0; p < depth; ++ p) {
                memoryMap[memoryMapIndex] = (byte) d;
                depthMap[memoryMapIndex] = (byte) d;
                memoryMapIndex ++;
            }
        }

可以看到,这里memoryMap和depthMap数组的大小是2^12=4096,memoryMap和depthMap初始里面都是维护二叉树中每个节点的位置和其对应的层高,比如memoryMap[1]=0 memoryMap[2]=1。
这个二叉树的大致结构如下:
在这里插入图片描述


首先会在本地线程缓存PoolThreadCache中尝试分配,Netty中给每个本地线程缓存了三个数组(实际是六个,heap和direct各三个)

    private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
    private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
    private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
    private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
    private final MemoryRegionCache<byte[]>[] normalHeapCaches;
    private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;

分别对应了tiny、samll、normal这三个内存级别。而这三个数组大小通过如下方式获取:

 DEFAULT_TINY_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.tinyCacheSize", 512);
DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.smallCacheSize", 256);
DEFAULT_NORMAL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.normalCacheSize", 64);

我们看下具体申请内存的过程,首先会根据请求待分配缓存的大小从上述对应的数组中查询,

private void allocate(PoolThreadCache cache, PooledByteBuf<T> buf, final int reqCapacity) {
        final int normCapacity = normalizeCapacity(reqCapacity);
        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;
                }
                tableIdx = tinyIdx(normCapacity);
                table = tinySubpagePools;
            } else {
                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;
                //默认情况下, 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) {
            if (cache.allocateNormal(this, buf, reqCapacity, normCapacity)) {
                return;
            }
            synchronized (this) {
                allocateNormal(buf, reqCapacity, normCapacity);
                ++allocationsNormal;
            }
        } else {
            allocateHuge(buf, reqCapacity);
        }
    }

如果在缓存中无法找到对应大小的内存空间,则会尝试在PoolChunkList上分配,如果还不行,最终会生成一个新的PoolChunk在其上面进行分配:

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;
        }
        PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
        boolean success = c.allocate(buf, reqCapacity, normCapacity);
        assert success;
        qInit.add(c);
    }
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;
        }
        PoolChunk<T> c = newChunk(pageSize, maxOrder, pageShifts, chunkSize);
        boolean success = c.allocate(buf, reqCapacity, normCapacity);
        assert success;
        qInit.add(c);
    }
// PoolChunk.java
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {
        final long handle;
        if ((normCapacity & subpageOverflowMask) != 0) { // >= pageSize
            handle =  allocateRun(normCapacity);
        } else {
            handle = allocateSubpage(normCapacity);
        }

        if (handle < 0) {
            return false;
        }
        ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;
        initBuf(buf, nioBuffer, handle, reqCapacity);
        return true;
    }

在PoolChunk上进行分配的时候,会分两种情况,>=pageSize(8KB)和小于8KB的情况:

>8KB 情况分配:
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 int allocateNode(int d) {
        int id = 1;
        int initial = - (1 << d); // has last d bits = 0 and rest all = 1
        byte val = value(id);
        if (val > d) { // 表明当前没有可以分配的空间
            return -1;
        }
        while (val < d || (id & initial) == 0) {// 每次将id左移一位,即向下移动一层
            id <<= 1;
            val = value(id);
            if (val > d) {
                id ^= 1;
                val = value(id);
            }
        }
        byte value = value(id); //找到节点高度值
        assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
                value, id & initial, d);
        setValue(id, unusable); // 标识相关节点不可用
        updateParentsAlloc(id); // 标识父节点不可用
        return id; // 返回id,1 ~ 2048*2 -1
    }
private byte value(int id) {
        return memoryMap[id];
    }
private void setValue(int id, byte val) {
        memoryMap[id] = val;
    }

可以看到,这里主要就是通过memoryMap来判断对应层节点和其下面的子节点是否有可用的待分配内存。这里我们讨论的是>=8KB内存大小的分配,在Netty中,PoolChunk每次申请都是16MB,然后会维护上面我们说的二叉树,借助这颗树,我们很容易的能对 >=8KB的内存大小找到一个合适位置
可以看到,这里

  1. 首先通过待分配的内存大小normCapacity获取其在二叉树哪一层
  2. 从二叉树的根节点开始判断当前节点及其子节点是否有空闲空间,首先判断根节点也就是memoryMap[1]的值是否大于当前节点的值,如果大于,表示下面没有该大小的可分配内存。
  3. 然后从根节点,每次将id左移一位,即按照,1,2,4,8,16这种方式,遍历,如果节点不可用,则通过id ^=1这种方式,来查询右兄弟节点及其子节点是否有可用(因为这时候该节点及其下面没有待分配大小的内存可用)如果满足条件遍历节点的层高小于待分配内存大小的层高继续向下遍历,这样一直遍历完之后,如果有可用内存块,则会被找到
  4. 将对应内存块节点设置为不可用,Netty中很简单,就是将对应节点的值设置为总层高+·1 memoryMap[id]=12,这个时候是表明,当前节点及其所有子节点都不可用
  5. 设置父节点状态,这里会比较当前节点和兄弟节点memoryMap中保存的值的大小,用小的那一个设置父节点的memoryMap中的值(小的表明当前节点或者子节点还有节点可以分配,如果节点的memoryMap值=12,表明当前节点及其子节点都不可用(默认最高只有11层))

这里我们在贴一下上面的二叉树结构。
在这里插入图片描述

这样对于>=8KB的大小,我们借助这颗二叉树,很方便的就能够分配好,分配好之后,接下来会对PooledByteBuf进行初始化,这个我们后面再说,

<=8KB 分配
private long allocateSubpage(int normCapacity) {
		// 这里主要是从PoolArena获取一个同步变量,上锁
        PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);
        int d = maxOrder; 
        synchronized (head) {
            int id = allocateNode(d);
            if (id < 0) {
                return id;
            }
            final PoolSubpage<T>[] subpages = this.subpages;
            final int pageSize = this.pageSize;
            freeBytes -= pageSize;
            int subpageIdx = subpageIdx(id);
            PoolSubpage<T> subpage = subpages[subpageIdx];
            if (subpage == null) {
                subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
                subpages[subpageIdx] = subpage;
            } else {
                subpage.init(head, normCapacity);
            }
            return subpage.allocate();
        }
    }
private int subpageIdx(int memoryMapIdx) {
        return memoryMapIdx ^ maxSubpageAllocs; // maxSubpageAllocs=2048
    }

可以看到,这里和>8KB的分配不同,>8KB是从根节点开始查找,而这里<=8KB是直接从叶子节点开始查找,因为叶子节点是这颗二叉树里面代表最小的内存块(8KB),我们看到,这里首先会根据返回的二叉树的节点ID(叶子节点ID),通过上面的图我们知道,二叉树的叶子节点一共是2048个块,而subpages数组的大小也是2048,也就是subpages数组每个元素正好对应到二叉树上的一个叶子节点。
需要注意的是这里和>8KB一样调用了allocateNode方法,即将叶子节点给PoolSubpage之后,该节点及其父节点就会更新节点的状态,后续无法分配
而这里memoryMapIdx ^ maxSubpageAllocs可以理解为将memoryMapIdx -2048正好将每个叶子节点在二叉树中的ID转换成在subpages数组中的下标。

这样我们通过返回叶子节点的id能够在subpages数组数组中找到对应的PoolSubpage
如果找到PoolSubpage为空,则进行实例化:

subpage = new PoolSubpage<T>(head, this, id, runOffset(id), pageSize, normCapacity);
PoolSubpage(PoolSubpage<T> head, PoolChunk<T> chunk, int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
        this.chunk = chunk;
        this.memoryMapIdx = memoryMapIdx;
        this.runOffset = runOffset;
        this.pageSize = pageSize;
        bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64
        init(head, elemSize);
    }
void init(PoolSubpage<T> head, int elemSize) {
        doNotDestroy = true;
        this.elemSize = elemSize;
        if (elemSize != 0) {
            maxNumElems = numAvail = pageSize / elemSize;
            nextAvail = 0;
            bitmapLength = maxNumElems >>> 6;
            if ((maxNumElems & 63) != 0) {
                bitmapLength ++;
            }

            for (int i = 0; i < bitmapLength; i ++) {
                bitmap[i] = 0; // 对bitmap数组中每个元素long的64位都初始化
            }
        }
        addToPool(head);
    }
    private void addToPool(PoolSubpage<T> head) {
        assert prev == null && next == null;
        prev = head;
        next = head.next;
        next.prev = this;
        head.next = this;
    }

这里有一个点需要注意,这里的PoolSubpage会加入到从PoolArenaSubpagePools数组找到的PoolSubpage的链表上,在找到的这个节点的后面插入。
这里面每次不管PoolSubpage有没有实例化,都会调用init
这里暂时看不懂没关系,我们看下,PoolSubpage怎么分配内存的:

long allocate() {
        if (elemSize == 0) {
            return toHandle(0);
        }
        if (numAvail == 0 || !doNotDestroy) {
            return -1;
        }
        final int bitmapIdx = getNextAvail();
        int q = bitmapIdx >>> 6;
        int r = bitmapIdx & 63;
        assert (bitmap[q] >>> r & 1) == 0;
        bitmap[q] |= 1L << r;
        if (-- numAvail == 0) {
            removeFromPool();
        }
        return toHandle(bitmapIdx);
    }
private long toHandle(int bitmapIdx) {
        return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
    }

好了,到这里我们来简单梳理下PoolSubpage的分配逻辑,在PoolSubpage中有如下bitmap:

bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64

长度为pagesize / 16 / 64 ,这里采用bitmap来标识一个内存单元是否被占用,在 PoolSubpage最小的内存单元为16B,然后依次按照16递增,用bitmap数组,每个数组用long,64位,可以表示64个内存块的使用情况,bitmap默认长度为8192/16/64=8
我们需要注意的一点是,如果这里能够找到一个PoolSubpage则表明在二叉树中的叶子节点是空闲的,并且之后这个二叉树节点会标记为已经分配,这个分配就是给了PoolSubpage。
每个PoolSubpage内存块的大小是固定的,在init的时候就会给他固定好的,init的时候传入的elemSize就是PoolSubpage中的内存块的大小
另外,在PooChunk的时候能找到一些PoolSubpage不为null,这表明这个PoolSubpage的二叉树叶子结点内存空闲了,可以分配了,这时候还是会调用init来重新设置内存块大小。
初始化的最后,会把这个PoolSubpage加入到PoolArenaSubpagePools数组中一个内存规格的链表上。

另外这里可以看到,我们每次初始PoolSubpage的时候实际上并不一定是16B,有可能大于16B,但是Netty中给bitmap这个数组大小就是固定在了8192/16/64,这样省的每次内存块变化时频繁对数组进行扩缩

这里通过bitmap我们能够找到哪些内存块没有分配,这里int q = bitmapIdx >>> 6能够获取对应的bitmapId在bitmap数组中的下标,然后bitmapIdx & 63能够获取到这个bitmapId在bitmap数组中元素的long类型的第几位,然后通过bitmap[q] |= 1L << r;进行左移,将该位设置为1,表示这个bitmap对应位置的内存块已经被分配。

final int bitmapIdx = getNextAvail();
int q = bitmapIdx >>> 6;
int r = bitmapIdx & 63;
bitmap[q] |= 1L << r;

另外这里有这样一段,我在下面给出说明

// 我们知道,bitmap中是用一个long64位来表示64个内存单元是否分配,这里maxNumElems >>> 6就是 maxNumElems / 64,
// 我们按照给定的内存规格划分的内存单元个数在除以64就是实际需要的bitmap的数组的长度,但是为了防止有些不是64的整数倍,而无符号左移会损失以为,
//这里单独判断划分的内存单元的个数能否被64整除,如果不能 +1 ,说白了就是先对待分配内存单元用64整除,得到一个数字,
// 然后看是不是能被64整除,不能再+1,netty中这么算就是为了能够贴近底层计算机运算逻辑,你在netty中很多地方能见到一般不会有常规运算,都是位运算
maxNumElems = numAvail = pageSize / elemSize;
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
	bitmapLength ++;
}

这里我们在啰嗦下,看看怎么找可分配的bitmapId的:

private int getNextAvail() {
        int nextAvail = this.nextAvail;
        if (nextAvail >= 0) {
            this.nextAvail = -1;
            return nextAvail;
        }
        return findNextAvail();
    }

    private int findNextAvail() {
        final long[] bitmap = this.bitmap;
        final int bitmapLength = this.bitmapLength;
        for (int i = 0; i < bitmapLength; i ++) {
            long bits = bitmap[i];
            // 取反,如果为0,表明各个位上都是1,这个64个内存单元已经分配完,不为0,表明还有可分配的空间
            if (~bits != 0) {
                return findNextAvail0(i, bits);
            }
        }
        return -1;
    }
    private int findNextAvail0(int i, long bits) {
        final int maxNumElems = this.maxNumElems;
        // baseVal是bitmap下标i中内存单元的起始序号
        final int baseVal = i << 6;
        // 这里从bits的64位的最低位一次和1进行&运算,如果==0表明该位置上的内存单元未被分配,否则bits继续无符号右移一位,判断下一位是否被分配
        for (int j = 0; j < 64; j ++) {
            if ((bits & 1) == 0) {
            	// baseVal是bitmap下标i中内存单元的起始序号,与j进行或运算,得到可分配内存单元的序号
                int val = baseVal | j;
                if (val < maxNumElems) {
                    return val;
                } else {
                    break;
                }
            }
            // 将bits无符号右移一位,继续判断
            bits >>>= 1;
        }
        return -1;
    }

这里上面都写了注释,其实逻辑很简单,比如这个PoolSubpage的内存规格是16B,则一共可以分配的内存单元是8192/16=512个内存单元,需要多少个bitmap呢 ?512/64=8,能够被整除,然后一次开始bitmap中每个元素,也就是long的每位上是否有带分配的内存,比如在bitmap[1]上的long的第32位找到可以分配的,那么这个待分配的内存单元的序号就是1*64+(32-1),就是85位
另外从这个查找结果可以看到,PoolSubpage是从bitmap中0,1,2开始查找分配,每个long的64位上也是从0,1,2开始查找分配。好了,到这里我们大概了解了PoolSubpage是怎么分配的。
到这里我们就理解了在Netty中如果在缓存中无法分配的化,通过新生成PoolChunk怎么分配>8KB & <=16MB>16B & <=8KB是怎么分配的。
另外,我们在PoolChunk.allocateSubpage的时候,首先是会根据内存规格从PoolArena对应的tinySubpagePools或smallSubpagePools中找到对应的内存规格的PoolSubpage链表的head,然后将其加入到其head.next中,注意这个是刚分配出来的PoolSubpage,而在PoolArena开始的时候首先就会从tinySubpagePools或smallSubpagePools中找对应的PoolSubpage,如果有表示能够分配,不会新生成PoolChunk,这里新生成的加入到PoolSubpage链表中,后面就会从这个PoolSubpage继续分配。同时会把这个新生成的PoolChunk放入到PoolArena的PoolChunkList中去。
前面我们还有一个PoolThreadCache上的分配还没讲,我们先看下Netty中怎么将内存释放的,因为PoolThreadCache上的内存都是释放后加入的


当我们进行内存回收释放时,一般代码如下:

byteBuf.release()
//或者
ReferenceCountUtil.release(byteBuf)

其最终调用的是:PooledByteBuf.deallocate:

    protected final void deallocate() {
        if (handle >= 0) {
            final long handle = this.handle;
            this.handle = -1;
            memory = null;
            chunk.arena.free(chunk, tmpNioBuf, handle, maxLength, cache);
            tmpNioBuf = null;
            chunk = null;
            recycle();
        }
    }
    private void recycle() {
        recyclerHandle.recycle(this);
    }

可以看到,PoolByteBuf对象会被回收,回收逻辑按照上面对象池的回收逻辑回收,实际内存的回收是交给了PoolArena处理:

void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
        if (chunk.unpooled) {
            int size = chunk.chunkSize();
            destroyChunk(chunk);
            activeBytesHuge.add(-size);
            deallocationsHuge.increment();
        } else {
            SizeClass sizeClass = sizeClass(normCapacity);
            if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
                return;
            }

            freeChunk(chunk, handle, sizeClass, nioBuffer, false);
        }
    }

对于池化和非池化的内存处理逻辑不一样,如果是非池化内存,则直接回收并释放,对于池化内存,会加入到缓存中去,如果缓存失败,则也会直接回收并释放。
加入缓存,调用了PoolThreadCache.add:

boolean add(PoolArena<?> area, PoolChunk chunk, ByteBuffer nioBuffer,
                long handle, int normCapacity, SizeClass sizeClass) {
        MemoryRegionCache<?> cache = cache(area, normCapacity, sizeClass);
        if (cache == null) {
            return false;
        }
        return cache.add(chunk, nioBuffer, handle);
    }

这里其实在PoolThreadCache中内存的处理和PoolArena中的tinySubpagePoolssmallSubpagePools处理类似,只不过,这里增加了一个>8KB & ,=16MB的规格,在PoolThreadCache维护了如下6个数组,其实就是堆内核堆外各三个

    private final MemoryRegionCache<byte[]>[] tinySubPageHeapCaches;
    private final MemoryRegionCache<byte[]>[] smallSubPageHeapCaches;
    private final MemoryRegionCache<ByteBuffer>[] tinySubPageDirectCaches;
    private final MemoryRegionCache<ByteBuffer>[] smallSubPageDirectCaches;
    private final MemoryRegionCache<byte[]>[] normalHeapCaches;
    private final MemoryRegionCache<ByteBuffer>[] normalDirectCaches;

这里缓存到本队线程的时候先会根据内存规格获取对应的MemoryRegionCache:

private MemoryRegionCache<?> cache(PoolArena<?> area, int normCapacity, SizeClass sizeClass) {
        switch (sizeClass) {
        case Normal:
            return cacheForNormal(area, normCapacity);
        case Small:
            return cacheForSmall(area, normCapacity);
        case Tiny:
            return cacheForTiny(area, normCapacity);
        default:
            throw new Error();
        }
    }
private MemoryRegionCache<?> cacheForTiny(PoolArena<?> area, int normCapacity) {
        int idx = PoolArena.tinyIdx(normCapacity);
        if (area.isDirect()) {
            return cache(tinySubPageDirectCaches, idx);
        }
        return cache(tinySubPageHeapCaches, idx);
    }
private MemoryRegionCache<?> cacheForSmall(PoolArena<?> area, int normCapacity) {
        int idx = PoolArena.smallIdx(normCapacity);
        if (area.isDirect()) {
            return cache(smallSubPageDirectCaches, idx);
        }
        return cache(smallSubPageHeapCaches, idx);
    }
private MemoryRegionCache<?> cacheForNormal(PoolArena<?> area, int normCapacity) {
        if (area.isDirect()) {
            int idx = log2(normCapacity >> numShiftsNormalDirect);
            return cache(normalDirectCaches, idx);
        }
        int idx = log2(normCapacity >> numShiftsNormalHeap);
        return cache(normalHeapCaches, idx);
    }

可以看到这里对Tiny和Small级别都是通过直接调用PoolArena相应的id获取逻辑,也就说明,在PoolThreadCache中tinySubPageDirectCaches和smallSubPageDirectCaches数组与PoolArena中的tinySubpagePools和smallSubpagePools主要逻辑都是一样的,每个数组元素都是一个固定大小的内存规格单元的链表(PoolThreadCache中通过MpscArrayQueue保存关系)
只不过,这里另外在加入了一个Normal级别的内存规格单元。
在PoolArena中并没有借助PoolSubpage去进行Normal级别的内存单元分配,而是直接基于PoolChunk里面维护的二叉树去进行分配的。

到这里,我门大概讲完了Netty中的内存分配,这里我们总结一下:

  1. 在Netty助攻是通过PooledByteBufAllocator来进行内存分配,在PooledByteBufAllocator维护了一个PoolArena<ByteBuffer>[] directArenas,然后大小与worker线程一样,然后会将其每个元素关联到一个PoolThreadCache本地线程变量中
  2. 每次分配都获取本地线程变量的PoolArena,由PoolArena去进行内存申请,PoolArena每次申请的时都是申请16MB大小内存(默认),然后交由PoolChunk去管理,每次申请时都会对待分配的内存的大小进行统一规范处理,生成的PoolChunk会加入到PoolArenaPoolChunkList链表中去
  3. PoolChunk中对16MB的内存进行分段处理,形成如下二叉树结构:

在这里插入图片描述

  1. 如果分配内存规格>8KB则直接在二叉树上分配,分配完之后,会标记对应节点的状态为已分配状态
  2. 如果待分配的内存规格<=8KB 则会对二叉树的叶子结点在进行细分,PoolChunk中维护了一个PoolSubpage<T>[] subpages;数组,里面每个元素代表的是一个内存单元,这时候会根据待分配内存单元,将可分配的叶子结点封装成一个PoolSubpage,里面会将待分配的内存单元规格,设置为PoolSubpage中每个内存块大小,同时根据内存块大小得到当前这个PoolSubpage有多少个可以分配的内存单元。另外会将生成的PoolSubpage加入到PoolArena中对应的tinySubpagePoolssmallSubpagePools中去
  3. 当内存释放的时候,会将释放的内存放入到本地线程缓存中去,与上面的tinySubpagePoolssmallSubpagePools一样,本地线程缓存中也维护这样的数组,但是还多一个,是normalDirectCaches, 规格为>8KB & <=16MB
  4. PoolArena中每次分配都会对待分配的内存大小规范化,Netty中内存规格如下:
Tiny < 512B
512B <= Small < 8KB (pageSize)
8KB <= Normal <= 16MB(chunkSize)
Huge > 16MB (不会进行池化缓存,分配完,直接回收)

当待分配内存<=16MB的时候,首先是先从本地线程缓存中查找有没有合适的待分配内存,如果没有对于tiny和small级别的,则从PoolArenaSubpagePools去尝试分配,如果分配不成功,尝试从PoolArenaChunkList中分配,如果还不成功,则生成一个新的Chunk去分配,对于>16MB的内存申请,直接分配,不缓存

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值