netty 第三方通讯包设计到底层源码剖析(二) 内存回收与释放

584 阅读4分钟

承接上文,来谈一谈ReferenceCountUtil.release函数的执行逻辑

原函数

public static boolean release(Object msg) {
        return msg instanceof ReferenceCounted ? ((ReferenceCounted)msg).release() : false;
    }

默认接口实现 AbstractReferenceCountedByteBuf.java

private boolean handleRelease(boolean result) {
        if (result) {
            this.deallocate();
        }

        return result;
    }
    
    protected final void deallocate() {
        ByteBuf parent = this.parent;
        this.recyclerHandle.recycle(this);
        parent.release();
    }

ReferenceCountednetty引用计数的类型,由于netty默认的消息类型是ByteBuf,所以尝试分析AbstractPooledDerivedByteBuf.deallocate()方法 实现 Recycle.recycle

public void recycle(Object object) {
            if (object != this.value) {
                throw new IllegalArgumentException("object does not belong to handle");
            } else {
                Recycler.Stack<?> stack = this.stack;
                if (this.lastRecycledId == this.recycleId && stack != null) {
                    stack.push(this);
                } else {
                    throw new IllegalStateException("recycled already");
                }
            }
        }

看出这里是利用了栈结构去维护调用的handler顺序,但netty只对最后一个handler传递的消息进行释放。因为netty只针对传递过程中,接口传入的值进行入栈,所以我们在使用的时候可能会遇到如下需要手动释放的场景

  • 如果在handlers传递过程中,传递了新值,老值需要你自己手动释放。

  • 如果中途没有使用fireChannelRead传递下去也要自己释放。

  • 如果在传递过程中自己通过Channel或ChannelHandlerContext创建的但是没有传递下去的ByteBuf也要手动释放。

对于前面两种目前可以理解,但为什么最后一种也需要呢? 因为netty使用了对象池

Netty利用ByteBuf对象池 BUFFER POOLING

Netty在对象池中利用引用计数器技术,为创建的ByteBuf对象设置引用计数器。 仅当引用计数器的值为0时,返回对象池回收。 新创建的引用计数对象的计数器的初始值为1:

ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;

释放引用计数对象时,其引用计数器的值减1。如果引用计数到达0,则引用计数对象将被释放或返回其对象池:

assert buf.refCnt() == 1;
// release() returns true only if the reference count becomes 0.
boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;

尝试访问引用计数器的值为0的引用计数对象将触发IllegalReferenceCountException异常:

assert buf.refCnt() == 0;
try {
  buf.writeLong(0xdeadbeef);
  throw new Error("should not reach here");
} catch (IllegalReferenceCountExeception e) {
  // Expected
}

可以看到AbstractPooledDerivedByteBuf先用栈保存了需要在哪个handler释放,但是最后还是调用了父类的方法,也就是池化内存PoolByteBuf的deallocate方法

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

    }

这里提到了chunk的概念,有点类似与c++内存碎片的单位内存块。此处netty采用了jemalloc的伙伴分配算法

netty的内存分配算法特征自然也需要满足以下几点

  • 大对象则以页为单位进行管理,配合小对象所在的页,降低碎片,设计一个好的存储方案(metadata)减少对内存的占用,同时优化内存信息的存储。以使对每个bin或大内存区域的访问性能最优且有上限。

  • 当释放内存时,要能够合并小内存为大内存,该保留的保留下次可快速响应,不该保留的释放给系统

  • 多线程环境下,每个线程可以独立的占有一段内存区间(TLS),这样线程内操作可以不加锁

让我们回头看看free函数

free函数

void free(PoolChunk<T> chunk, ByteBuffer nioBuffer, long handle, int normCapacity, PoolThreadCache cache) {
        if (chunk.unpooled) { //如果对象未池化,说明未回收
            int size = chunk.chunkSize(); //获取当前内存大小
            this.destroyChunk(chunk);
            this.activeBytesHuge.add((long)(-size));//需要回收的内存大小
            this.deallocationsHuge.increment();//因为还未回收,引用计数+1
        } else {
            PoolArena.SizeClass sizeClass = this.sizeClass(normCapacity);
            if (cache != null && cache.add(this, chunk, nioBuffer, handle, normCapacity, sizeClass)) {
                return;
            }

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

    }
    
        void freeChunk(PoolChunk<T> chunk, long handle, PoolArena.SizeClass sizeClass, ByteBuffer nioBuffer, boolean finalizer) {
        boolean destroyChunk;
        synchronized(this) {
            if (!finalizer) {
                switch(sizeClass) {
                case Normal:
                    ++this.deallocationsNormal;
                    break;
                case Small:
                    ++this.deallocationsSmall;
                    break;
                case Tiny:
                    ++this.deallocationsTiny;
                    break;
                default:
                    throw new Error();
                }
            }

            destroyChunk = !chunk.parent.free(chunk, handle, nioBuffer);
        }

        if (destroyChunk) {
            this.destroyChunk(chunk);
        }

    }

进入内部的free函数

void free(long handle, ByteBuffer nioBuffer) {
        int memoryMapIdx = memoryMapIdx(handle);
        int bitmapIdx = bitmapIdx(handle);
        if (bitmapIdx != 0) {
            PoolSubpage<T> subpage = this.subpages[this.subpageIdx(memoryMapIdx)];

            assert subpage != null && subpage.doNotDestroy;

            PoolSubpage<T> head = this.arena.findSubpagePoolHead(subpage.elemSize);
            synchronized(head) {
                if (subpage.free(head, bitmapIdx & 1073741823)) //使用位图法,分页回收{
                    return;
                }
            }
        }

仔细思考一下,发现回收内存碎片其实可以分为两步,在内存初始化的时候根据回收算法中二叉树的子节点编号标记成位图,然后再释放的时候按子节点大小查找对应的位图标记转换成二进制串,根据节点的类型进行移动位置。不再引用的内存在下一次JVM GC的时候会被标记为内存碎片回收掉

netty针对SSL/TLS的内存释放

ReferenceCountedOpenSslContext.java的release方法实现

同样,释放内存的时候也是调用了deallocate方法

public final boolean release() {
        return this.refCnt.release();
    }
    
    private boolean release0(int decrement) {
        int oldRef = refCntUpdater.getAndAdd(this, -decrement);
        if (oldRef == decrement) {
            this.deallocate();
            return true;
        } else if (oldRef >= decrement && oldRef - decrement <= oldRef) {
            return false;
        } else {
            refCntUpdater.getAndAdd(this, decrement);
            throw new IllegalReferenceCountException(oldRef, -decrement);
        }
    }

Continue..QwQ