Netty源码分析之ByteBuf

一、Java中的Buffer对象

我们知道Buffer是NIO底层的重要组件之一,netty沿用了NIO中的Buffer实现,并且进行了更进一步的封装,所以我们先来了解一下NIO中的Buffer。

Buffer中有三个非常重要的属性,源码中的注解是这样描述的:

capacity:表示一个buffer的容量,这个值一旦确定就不会被改变,类似于一位数组,数组长度一旦确认就不会在变。

limit:表示,第一个元素不能被读或写的索引,并且不能大于capacity。

position:是下一个能被读或写的元素索引。position不能大于limit

相关的读写方法,会从position位置开始,并且随着读写的进行,position会自动的递增操作,如果读写请求的索引值超过了limit,此时就会抛出一个BufferUnderflowException异常。如果是通过索引下标去操作buffer,超过limit就会抛出IndexOutOfBoundsException异常。

Buffer中还有一个重要的属性就是mark,mark初始值为-1,mark是存储position的的一个标志属性。当mark方法执行之后,就会被设置成position,并且mark不能大于position。通过调用reset方法,mark归位。position变为mark值。如果position或者limit被调整成比mark还小的值。position就会被归位。如果mark值为-1的时候调用reset方法就会抛出InvalidMarkException异常。

Buffer为了操作这些属性,Buffer定义了一些可操作的方法

clear():这个方法会把limit设置成capacity,position设置成0,这样方便读和put操作

flip():这个方法会把limit设置成position然后把position设置成0,方便读取操作

rewind():这个方法,会把position设置成0,limit保持不变。也就是方便重读。

以上是对Buffer进行的简单分析。

二、ByteBuf

ByteBuf是netty中的重要组件之一,也是netty的核心数据结构。ByteBuf接口继承自RefeenceCounted,采用的是引用计数法,来判断当前的Buffer是否可以回收。RefeenceCounted中有两个方法,一个是retain一个是release,当你引用了buf就需要调用一下retain,不使用的时候调用一下release释放。当refCount为0的时候表示可以进行内存回收。

ByteBuf中维护着两个索引,分别是readerIndex和writeIndex两个索引把buffer分成了三个区域,无效区、可读区、可写区。可读区是实际内容被存储的区域,任何带有read和skip的方法都是操作这个区域,当索引超出这个区域就会抛出IndexOutOfBoundsException异常。可写区,就是数据即将被写入的区域,当超出就会抛出IndexOutOfBoundsException异常,任何带有write的方法都会操作这个区域。无效区就是已经读取的数据区域,当调用discardReadBytes()就会清除掉这个区域的数据,然后把可读区移动到最开始的位置。还有一个clear方法,这个方法就是清楚所有的数据,也就是把writeIndex置0。

回到源码AbstractNioByteChannel内部类NioByteUnsafe的read方法。

 ByteBuf byteBuf = allocHandle.allocate(allocator);//是通过Allocator分配得来

三、ByteBufAllocator

ByteBufAllocator allocator = config.getAllocator();//这也是在read方法中分配的

查看配置类中可知,Allocator是通过如下代码来分配的:

ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;

继续查看接口,发现默认的Allocator是通过工具类来分配:

ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
    static final ByteBufAllocator DEFAULT_ALLOCATOR;

    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;
    }

根据io.netty.allocator.type系统配置,获取是池化的还是非池化的配置(区别就在于内存使用完之后否是立即回收,立即回收的就是非池化的,循环使用的就是池化的),默认是池化的。

所以read方法中得到的Allocator是PooledByteBufAllocator实例。netty中的ByteBuf对象都是通过这个Allocator来分配。

四、Handle

回到NioByteUnsafe的read方法,我们可以发现,Buffer的分配离不开Handle,那么Handle具体是什么?我们从源码一探究竟。

public final void read() {
            final ChannelConfig config = config();
            //获取到实际的Allocator
            final ByteBufAllocator allocator = config.getAllocator();
            //实例化一个handle
            final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
            allocHandle.reset(config);
            ByteBuf byteBuf = null;
                do {//这里可知buffer的分配入口是在handle中
                    byteBuf = allocHandle.allocate(allocator);
                    //
                    allocHandle.lastBytesRead(doReadBytes(byteBuf));
                    if (allocHandle.lastBytesRead() <= 0) {
                        close = allocHandle.lastBytesRead() < 0;
                    }
                    allocHandle.incMessagesRead(1);
                } while (allocHandle.continueReading());
                allocHandle.readComplete();

        }
    }

Handle是RecvByteBufAllocator的内部类,具体的分配是通过RecvByteBufAllocator中的newHandle方法分配。分析可知RecvByteBufAllocator的默认实现是new AdaptiveRecvByteBufAllocator()。这是在config中实现的。所以recvBufAllocHandle()中的newHandle方法就是在AdaptiveRecvByteBufAllocator实现。newHandle()->new HandleImpl(minIndex, maxIndex, initial),三个参数的默认值分别是64、65536、2048,我们先不管这三个参数值具体含义与作用实际分析过程再去了解。所以我们最终得到的是HandleImpl的实现。
allocHandle.allocate(allocator):这是分配的入口,方法的具体实现在MaxMessageHandle中,->alloc.ioBuffer(guess());其中guess()方法获取到的是初始化容量大小。方法实际返回的是HandleImpl中的nextReceiveBufferSize参数。顾名思义就是即将接收的buffer大小,那么这个参数值是怎么定义的呢?继续查看HandleImpl的构造方法。

        HandleImpl(int minIndex, int maxIndex, int initial) {
            this.minIndex = minIndex;
            this.maxIndex = maxIndex;
            //根据initial获取数组下标值,根据上边的分析他的默认值是2048
            index = getSizeTableIndex(initial);
            nextReceiveBufferSize = SIZE_TABLE[index];
        }

getSizeTableIndex():方法

    private static int getSizeTableIndex(final int size) {
        for (int low = 0, high = SIZE_TABLE.length - 1;;) {
            if (high < low) {
                return low;
            }
            if (high == low) {
                return high;
            }

            int mid = low + high >>> 1;
            int a = SIZE_TABLE[mid];
            int b = SIZE_TABLE[mid + 1];
            if (size > b) {
                low = mid + 1;
            } else if (size < a) {
                high = mid - 1;
            } else if (size == a) {
                return mid;
            } else {
                return mid + 1;
            }
        }
    }

getSizeTableIndex()方法就是通过二分法查找,找到最接近initial初始值的在SIZE_TABLE中的下标值。SIZE_TABLE中存储的是小于512的都是按照16的倍数存储,超过512的都是按照512的不断乘以2来存储。所以不管初始值设置的是多少都会递归到2的n次幂上来。所以nextReceiveBufferSize 的初始值是2048.
allocHandle.lastBytesRead(doReadBytes(byteBuf)):这个方法会记录实际读取到的数据长度
allocHandle.incMessagesRead(1):数据读取次数加一操作(totalMessages+1)
allocHandle.continueReading():判断当前是否需要继续读取数据操作,其中一个比较就是totalMessages < maxMessagePerRead(maxMessagePerRead的值默认是16),所以当消息读取次数超过了16次就会跳出循环。
allocHandle.readComplete():这个方法的主要目的是计算出nextReceiveBufferSize值,根据之前的读取结果是增加还是减少。

Handle总结:

    public interface Handle {
        ByteBuf allocate(ByteBufAllocator var1);//分配Buffer
        int guess();//返回当前能读取的最大字节数
        void reset(ChannelConfig var1);//重置maxMessagePerRead、totalMessages、totalBytesRead值
        void incMessagesRead(int var1);//每次读取之后加一
        void lastBytesRead(int var1);//记录当前读取的字节数、以及循环读取的总字节数,根据需要更新nextReceiveBufferSize
        int lastBytesRead();//返回最新读取的字节数
        void attemptedBytesRead(int var1);//保存预期要读取的字节数
        int attemptedBytesRead();
        boolean continueReading();//判断当前循环是否需要结束
        void readComplete();//完成读取,这个方法会根据当前读取到的字节数,跟当前的设置的预期读取长度进行比较要小,那么就缩小当前设置的nextReceiveBufferSize,相反增加,从源码中可知小的时候,index-1,大于的时候index+4,直到65536。
    }

Handle可以理解为不断的更新当前channel读取数据的容量,默认的最大容量为65536,也可以理解Allocator分配Buffer的时候的最大容量是通过Handle来分配。

五、分配过程

以上过程都是为了实现Buffer做准备,我们知道ByteBuf的分配是通过Allocator来实现,他的入口方法在Handle中的allocate(ByteBufAllocator var1)–>alloc.ioBuffer(guess()),guess()方法返回的就是需要分配的容量大小。

    public ByteBuf ioBuffer(int initialCapacity) {
        if (PlatformDependent.hasUnsafe() || isDirectBufferPooled()) {
            return directBuffer(initialCapacity);//走直接内存(默认)
        }
        return heapBuffer(initialCapacity);//走堆内存
    }
    @Override
    public ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
        if (initialCapacity == 0 && maxCapacity == 0) {
            return emptyBuf;
        }
        validate(initialCapacity, maxCapacity);
        return newDirectBuffer(initialCapacity, maxCapacity);//这是一个抽象方法,具体的实现在子类中这里就分为Pooled和Unpooled两种方式(默认是Pooled)
    }

以下就是newDirectBuffer方法在PooledByteBufAllocator中的具体实现。

    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);//否则返回一个非缓存池的buffer
        }

        return toLeakAwareBuffer(buf);//这里是包装成可以内存泄露感知的buffer,具体是怎么进行内存泄露感知的后续将会单独介绍
    }

以上就是一个简单分配过程,实际的内存分配过程非常复杂,需要一点点的去分析。

以上,有任何不对的地方,请指正,敬请谅解。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菜鸟+1024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值