什么?你不知道Netty中的ByteBuffer是什么,我来告诉你

        创作内容丰富的干货文章很费心力,感谢点过此文章的读者,点一个关注鼓励一下作者,激励他分享更多的精彩好文,谢谢大家!


        在Netty中存储数据和发送数据作为临时存储抽象的是ByteBuf,而在Java的NIO层面使用的是ByteBuffer。

ByteBuffer是什么

        如果不使用Netty进行发送或接收数据的操作,就需要使用Java抽象的ByteBuffer这个类。用户把数据通过ByteBuffer存储到内存中,之后再由操作系统进行之后的操作。这些数据存放到内存中,操作系统为它们申请了一块内存空间,这块内存空间就用字节数组包装了一下。内存中会创建一个ByteBuffer对象,ByteBuffer对象内部持有了字节数组的引用。用户只要获得ByteBuffer这个对象,就可以获得内部持有的字节数组的引用,之后就可以把数据以字节的方式存储到这个字节数组中,也就存放到申请的内存里了。看一下ByteBuffer的代码段更加的直观。

public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
    // 这个就是内部持有的字节数组,在构造方法中被赋值。
    final byte[] hb;  
}

        这个设计有一个弊端。创建的ByteBuffer对象是在堆内存中的,是在用户态中操作的,真正的发送或保存数据是操作系统在内核态中进行操作的。所以要想让操作系统正常进行操作,还需要把数据复制到堆外内存,在高并发网络I/O操作中,这无疑是很消耗性能的。更好的方式是在堆外申请一块内存给这些数据使用,用户的程序也能操作这些内存。实现的方式是让堆内存中创建的对象持有堆外内存的地址,这样用户程序可以直接向堆外内存写入数据,内核态也可以直接处理这些数据了。JDK已经帮我们实现了,ByteBuffer有两个子类,分别是DirectByteBuffer和HeapByteBuffer。

        ByteBuffer的结构如下图所示:

        ByteBuffer中有四个属性,下面来介绍一下。

  • mark:为某个读取过的关键位置做标记,方便回退到该位置。

  • position:当前可读取或可写入的位置。

  • limit:ByteBuffer中有效的数据长度大小。

  • capacity:初始化时的空间容量。

        以上四个属性的大小关系是,mark<=position<=limit<=capacity。

ByteBuffer的缺陷
  • ByteBuffer分配的长度是固定的,无法动态扩容,很难控制需要分配多大的容量。如果分配太大容量,容易造成内存浪费。如果分配太小,存放太大的数据会抛出BufferOverflowException异常。在使用ByteBuffer进行开发时,需要每次判断数据需要的容量是否超过了上限,如果超过了,就需要重新申请新的容量空间,整个过程对开发者而言相对繁琐,不友好。

  • ByteBuffer只能通过position获取当前可读取或可写入的位置,因为读写公用position指针,所以需要频繁调用flip、rewind方法切换读写状态,开发者必须很小心处理ByteBuffer的数据读写,稍不留意就会出错。

    ByteBuffer继承自Buffer,我们先来看看Buffer这个类的关键代码。

    public abstract class Buffer {
    
        private int mark = -1;
        // 读写共用同一个指针
        private int position = 0;
        private int limit;
        private int capacity;
    
        // 如果使用的是堆外内存,那么堆外内存分配的内存地址就会赋值给这个成员变量
        long address;
    }

    接下来看看ByteBuffer类的关键代码。

    public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
    {
        // 堆内存保存消息的字节数组
        final byte[] hb;  
        // 内存偏移量                
        final int offset;
        // 是否为只读
        boolean isReadOnly;   
    
        ByteBuffer(int mark, int pos, int lim, int cap, byte[] hb, int offset)
        {
            super(mark, pos, lim, cap);
            this.hb = hb;
            this.offset = offset;
        }
    
        // 该方法创建的是一个直接缓冲区对象。
        public static ByteBuffer allocateDirect(int capacity) {
            return new DirectByteBuffer(capacity);
        }  
    
        // 创建堆缓冲区对象。
        public static ByteBuffer allocate(int capacity) {
            if (capacity < 0)
                throw new IllegalArgumentException();
            return new HeapByteBuffer(capacity, capacity);
        }  
    
        // 将一个字节数组包装到ByteBuffer中,创建的是堆缓冲区对象。
        public static ByteBuffer wrap(byte[] array,
                                        int offset, int length)
        {
            try {
                return new HeapByteBuffer(array, offset, length);
            } catch (IllegalArgumentException x) {
                throw new IndexOutOfBoundsException();
            }
        }
    }

    接下来看看HeapByteBuffer类的关键代码。

class HeapByteBuffer extends ByteBuffer {

    HeapByteBuffer(int cap, int lim) {            
        // 内存偏移量设置为0,把要使用的字节数组创建出来,然后复制给父类中的成员变量
        super(-1, 0, lim, cap, new byte[cap], 0);
    }  

    // 下面这两个就是从字节数组中获得字节的方法
    // 可以看到就是直接返回数组对应索引的字节
    public byte get() {
        return hb[ix(nextGetIndex())];
    }

    public byte get(int i) {
        return hb[ix(checkIndex(i))];
    }

    // 下面这两个就是向字节数组中写入字节的方法
    // 可以看到就是把字节直接放到字节数组对应的索引位置
    public ByteBuffer put(byte x) {

        hb[ix(nextPutIndex())] = x;
        return this;
    }

    public ByteBuffer put(int i, byte x) {

        hb[ix(checkIndex(i))] = x;
        return this;
    }
}

接下来看看DirectByteBuffer类的关键代码。

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{
    // 使用Unsafe类的对象分配堆外内存
    protected static final Unsafe unsafe = Bits.unsafe(); 

    DirectByteBuffer(int cap) {           
        super(-1, 0, cap, cap);
        // 直接内存是否按页对齐
        boolean pa = VM.isDirectMemoryPageAligned();
        // 得到每一页的大小
        int ps = Bits.pageSize();
        // 在要申请的内存cap的基础上在多增加一页数据大小ps,得到真正要分配的内存大小。
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        // 先尝试预留内存操作,如果没成功,调用Sytem.gc()回收内存,接下来在一个
        // 死循环中继续进行预留内存操作,这时候回收内存操作依靠虚拟机内存回收机制。
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            // Unsafe对象分配内存
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        // 初始化内存,默认每一位都是0
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        // 创建的cleaner对象,关联了创建的base堆外内存地址,当DirectByteBuffer
        // 对象被VM垃圾回收的时候,会根据base堆外地址,释放堆外内存。
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

    public byte get() {
        return ((unsafe.getByte(ix(nextGetIndex()))));
    }

    public byte get(int i) {
        return ((unsafe.getByte(ix(checkIndex(i)))));
    }

    public ByteBuffer put(byte x) {
        unsafe.putByte(ix(nextPutIndex()), ((x)));
        return this
    }

    public ByteBuffer put(int i, byte x) {
        unsafe.putByte(ix(checkIndex(i)), ((x)));
        return this;
    }

    private long ix(int i) {
        return address + ((long)i << 0);
    }
}
ByteBuf是什么

        经过了上面的详细介绍后,我相信ByteBuffer已经没有秘密可言了。下面我们本篇文章的主角ByteBuf就登场了。使用Netty进行开发也需要频繁的创建对象来存储数据,频繁的对象创建和回收操作,意味着性能的低下。所以Netty使用了池化的思想,把对象的创建进行管理。如果用户也可以不使用池化的ByteBuf。这就使得Netty中的ByteBuf不仅可以按照池化缓冲区对象和堆内缓冲区对象来细分,还可以按照池化缓冲区对象和非池化缓冲区对象细分。下面的代码列出了Netty中的几种缓冲区的类型。

// 非池化的堆内缓冲区
UnpooledHeapByteBuf
// 非池化的堆外缓冲区
UnpooledDirectByteBuf
// 池化的堆内缓冲区
PooledHeapByteBuf
// 池化的堆外缓冲区
PooledDirectByteBuf

        ByteBuf内部结构如下图所示:

        ByteBuf基于引用计数设计的,实现了ReferenceCounted接口。ByteBuf的生命周期是由引用计数所管理。只要引用计数大于0,表示ByteBuf还在被使用。当ByteBuf不再被其他对象所引用时,引用计数为0,那么代表该对象可以被释放,可以放入对象池中。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值