ByteBuf释放注意的问题

总结

Bytebuf需要释放,否则可能导致OOM。
如果bytebbuf传递到了head或tail,不需要我们关心。
在head和tail里(head实现了outhandler、inhander。tail实现了inhander),底层自动调用了bytebuf.release。
其他情况需要我们手动释放。 注意simpleinboundHandler特殊
在这里插入图片描述

netty默认使用的直接内存(也叫非堆内存或堆外内存),它创建、销毁的开销同比堆内存大,所以用池化来复用,如果使用直接内存而不用池化,性价比可能还不如堆内存。
直接内存可以减少用户态和内核态间的一次数据复制,所以在IO网络中优势高。如果不涉及IO网络操作,只是单纯操作内存,使用堆内存更好。
池化也能用在堆内存上,所以一共有四种:池化直接内存、池化堆内存、非池化直接内存、非池化堆内存。

ByteBuf模型

使用时会自动扩容,可扩容部分就是下图紫色。
byteBuf的结构

创建ByteBuf

        ByteBuf poolHeapBuf = ByteBufAllocator.DEFAULT.heapBuffer();
        ByteBuf poolDiredtBuf = ByteBufAllocator.DEFAULT.directBuffer();

        ByteBuf unpoolHeapBuf = Unpooled.buffer();
        ByteBuf unpoolDirBuf = Unpooled.directBuffer();
        System.out.println(poolHeapBuf);
        System.out.println(poolDiredtBuf);
        System.out.println(unpoolHeapBuf);
        System.out.println(unpoolDirBuf);

前两种是否是池化,需要看-Dio.netty.allocator.type=unpooled|pooled,如果是pooled,则二者都是池化内存。
后两种一定是非池化内存。
参数为pooled时,输出如下。实际中,还可以用ChannelHandlerContext ctx来分配。

PooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 256)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)
UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 256)
UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(ridx: 0, widx: 0, cap: 256)

李林峰在书中指出,每次默认开一个64KB大小的数组,导致了OOM。

结合代码分析发现,API网关每次收到请求消息,无论请求消息大还是小,哪怕只有100字节,都会构造一个默认为64KB的char数组,用于处理和转发请求消息。如果后端转发消息较慢,就会导致任务队列积压,由于每个任务(Runnable,此处为 Lambda 表达式)持有一个64KB的char数组,所以积压多了就会转移到老年代,一旦触发老年代GC,耗时较长,就会导致系统吞吐量降为0。

在这里插入图片描述

netty原码释放逻辑

当多个handler串起来时,数据会沿着链传统。用inboundhandler举例。
如何唤起下一个inhandler呢?可以调用fireChannelRead

class ServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.fireChannelRead(msg);
    }
}

底层就是:

    @Override
    public ChannelHandlerContext fireChannelRead(final Object msg) {
        invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
        return this;
    }

findContextInbound就是找到下一个handler。
invokeChannelRead就是调用下一个handler的invokeChannelRead,代码如下。
始终让Niothread去执行invokeChannelRead。并且执行的最后一个肯定是tail。

    static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
        final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
        EventExecutor executor = next.executor(); // 该context所属的线程
        if (executor.inEventLoop()) {
            next.invokeChannelRead(m);
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRead(m);
                }
            });
        }
    }

然后执行tail里面的代码
尤其是看看注释是啥:用户在channelRead里面没有处理message 并且该message传递到了tail节点,那么tail节点就会负责释放内存。

    /**
     * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
     * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
     * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
     */
    protected void onUnhandledInboundMessage(Object msg) {
        try {
            logger.debug(
                    "Discarded inbound message {} that reached at the tail of the pipeline. " +
                            "Please check your pipeline configuration.", msg);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }

为什么始终让Niothread去执行invokeChannelRead?
假如我们这样做,Netty还是会交给NIO线程去释放。

 new Thread(() -> {
            ctx.fireChannelRead(msg);
        }).start();

Netty 中的 invokeChannelRead 方法必须在 NIO 线程池中执行,以确保网络读事件的处理是高效、可靠和非阻塞的。

simpleinboundHandler

他为什么特殊?他在自己的finally快里面就释放了

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        boolean release = true;
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                I imsg = (I) msg;
                channelRead0(ctx, imsg);
            } else {
                release = false;
                ctx.fireChannelRead(msg);
            }
        } finally {
            if (autoRelease && release) {
                ReferenceCountUtil.release(msg);
            }
        }
    }

所以如果实现了sampleinboundhandler,byteBuf不用手动释放,也不用传递到tail去。

手动释放

调用release()方法即可。
有特别注意的点。
ByteBuf的一些方法如slice()、duplicate、compositeByteBuf等,都是基于原数组的引用。
如果你释放掉了,其他人持有该引用写入就会发生问题。
最好是每调用上述方法一次,就引用次数+1retain(),谁使用完,谁就release掉。

    public static void main(String[] args) throws Exception {
        ByteBuf poolHeapBuf = ByteBufAllocator.DEFAULT.heapBuffer(10);
        // 模拟多人使用
        use(poolHeapBuf);
        use(poolHeapBuf);
    }

    private static void use(ByteBuf poolHeapBuf) {
        ByteBuf slice = poolHeapBuf.slice(0, 3);
        slice.retain();
        // 执行逻辑完,最后释放掉。
        slice.release();
    }

再次理解netty释放逻辑

ReferenceCountUtil.release(msg);底层调用的这个方法,
再次进入源码,发现只要实现了ReferenceCounted接口的,都会被释放掉。

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

他的实现有很多,包括前面的各种byteBuf,还有http相关,使用时要注意
在这里插入图片描述

本文作者:WKP9418
本文地址:https://blog.csdn.net/qq_43179428/article/details/140564408

  • 36
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值