netty中的ByteBuf源码详解
作者:波波仔86
一、背景简介
ByteBuf,顾名思义,就是字节缓冲区,是Netty中非常重要的一个组件。熟悉jdk NIO的同学应该知道ByteBuffer,正是因为jdk原生ByteBuffer使用比较复杂,某些场景下性能不是太好,netty开发团队重新设计了ByteBuf用以替代原生ByteBuffer。
二、ByteBuf和ByteBuffer对比
下面用图示来展示ByteBuf和ByteBuffer工作原理:
ByteBuffer
ByteBuffer依靠flip()来切换模式,在读模式下调用flip()切换为写模式,在写模式下limit和capacity相等,position标识当前写的位置。在写模式下调用flip()切换为读模式,在读模式下position回到起始位置开始读,limit回到position位置表示能读到多少数据,capacity不变表示缓存区容量大小。
capacity:在读/写模式下都是固定的,就是缓冲区容量大小。
position:读/写位置指针,表示当前读(写)到什么位置。
limit:在写模式下表示最多能写入多少数据,此时和capacity相同。在读模式下表示最多能读多少数据,此时它的值等于缓存区中实际数据量的大小。
ByteBuf
ByteBuf主要是通过readerIndex和writerIndex两个指针进行数据的读和写,整个ByteBuf被这两个指针最多分成三个部分,分别是可丢弃部分,可读部分和可写部分
刚初始化的时候,整个缓冲区还没有数据,读写指针都指向0,所有的内容都是可写部分,此时还没有可读部分和可丢弃部分,如下:
当写完N个字节数据后,读指针仍然是0,因为还没有开始进行读事件,写指针向后移动了N个字节的位置,如下:
当开始读数据并且读取M个字节数据之后(M<N)写指针位置不变,读指针后移动了M个字节的位置,如下:
当可丢弃部分数据被清空之后,readerindex重新回到起始位置,writerindex的位置为writerindex的值减去之前的readerindex,也就是M,相关图示如下:
调用clear之后,writerindex和readerinde全部复位为0。它不会清除缓冲区内容(例如,用填充0),而只是清除两个指针。
更改的读写指针的值,每个位置上原本的字节内容并没有发生改变,只是变成了可写状态而已。
另请注意,此操作的语义不同于Buffer.clear()。
三、源码
明白了ByteBuf工作原理之后,ByteBuf相关的api就很好理解了,在此附上netty官方api文档,以供参阅
我们在这里看下netty扩容相关源码逻辑。
扩容肯定是在写入数据的时候会由相关逻辑判断,我们随便进入一个写入字节的api方法。
public abstract ByteBuf writeBytes(byte[] src);
进入到其抽象子类AbstractByteBuf中。
@Override public ByteBuf writeBytes(byte[] src) { writeBytes(src, 0, src.length); return this; } @Override public ByteBuf writeBytes(byte[] src, int srcIndex, int length) { ensureAccessible(); ensureWritable(length); setBytes(writerIndex, src, srcIndex, length); writerIndex += length; return this; }
首先ensureAccessible进行安全校验,每种尝试访问缓冲区内容的方法都应调用此方法,以检查缓冲区是否已释放。然后ensureWritable判断是否可写,扩容相关逻辑就在这里进行判断,如果缓冲区可写执行setBytes进行数据写入,然后writerindex向后移动length的位置,最后将ByteBuf对象进行返回。我们重点看ensureWritable。
@Override public ByteBuf ensureWritable(int minWritableBytes) { if (minWritableBytes < 0) { throw new IllegalArgumentException(String.format( "minWritableBytes: %d (expected: >= 0)", minWritableBytes)); } ensureWritable0(minWritableBytes); return this; }
直接进入ensureWritable0(minWritableBytes)方法中,此时minWritableBytes就是我们计划需要申请的内存大小空间。
private void ensureWritable0(int minWritableBytes) { // 安全检查,保证写入之前是可访问的 //ensureAccessible(); // 可写,不必扩容 if (minWritableBytes <= writableBytes()) { return; } //下标越界 if (minWritableBytes > maxCapacity - writerIndex) { throw new IndexOutOfBoundsException(String.format( "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", writerIndex, minWritableBytes, maxCapacity, this)); } //达到临界条件,开始执行扩容逻辑 // 计算新的容量,实际上为当前容量扩容至2的幂次方大小(具体是多少需要进行后续判断和计算) int newCapacity = alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity); // 扩容后的容量 capacity(newCapacity); }
可以看到真正开辟内存空间新容量逻辑处理的是 alloc().calculateNewCapacity(writerIndex + minWritableBytes, maxCapacity)执行的,进入到方法里面。
来到其实现类AbstractByteBufAllocator的calculateNewCapacity方法。
@Override public int calculateNewCapacity(int minNewCapacity, int maxCapacity) { if (minNewCapacity < 0) { throw new IllegalArgumentException("minNewCapacity: " + minNewCapacity + " (expectd: 0+)"); } if (minNewCapacity > maxCapacity) { throw new IllegalArgumentException(String.format( "minNewCapacity: %d (expected: not greater than maxCapacity(%d)", minNewCapacity, maxCapacity)); } // 扩容的阈值,4兆字节大小 final int threshold = 1048576 * 4; if (minNewCapacity == threshold) { return threshold; } //如果计划一共需要的内存容量大小大于阈值,则需要和最大容量j进行比较 if (minNewCapacity > threshold) { int newCapacity = minNewCapacity / threshold * threshold; if (newCapacity + threshold > maxCapacity) { newCapacity = maxCapacity; } else { newCapacity += threshold; } return newCapacity; } //如果计划一共需要的内存容量大小小于阈值,则以64为基数进行倍增 int newCapacity = 64; while (newCapacity < minNewCapacity) { newCapacity <<= 1; } return Math.min(newCapacity, maxCapacity); }
minNewCapacity是我们计划一共需要的内存容量大小,maxCapacity是最大缓冲区容量大小。首先判断minNewCapacity 是否小于零或者minNewCapacity 是否大于maxCapacity,满足任一都抛出异常信息,然后判断我们计划一共需要的内存容量大小minNewCapacity 是否等于了阈值4M:
①、如果等于了阈值,新容量大小就是阈值4M。
②、如果计划一共需要的内存容量大小大于阈值,则maxCapacity和minNewCapacity 相对于阈值的整数倍再加上一个阈值进行大小判断,如果大于maxCapacity,则新容量最大就是maxCapacity,返回maxCapacity,如果小于maxCapacity,则相当于按照阈值的2倍进行扩容。
③、如果计划一共需要的内存容量大小小于阈值,则以64为基数只要小于我们计划需要的内存容量大小,就2倍扩容,最后选取循环后的扩容值和最大值两个值其中的较小者。
至此扩容就完成了,总结来说就是在扩容过程中有一个扩容需要容量的一个阈值4M,如果我们需要的内存空间等于这个阈值,那么扩容后的容量就是阈值大小,如果我们需要的内存容量大小大于阈值或者小于阈值,其扩容逻辑判断和扩容后返回的容量大小是不同的。但是最终扩容后的容量大小总是2的幂次方大小并且不会比maxCapacity大。
四、ByteBuf主要的继承关系
从内存分配的角度看,ByteBuf可以分为两类
(1)堆内存(HeapByteBuf)字节缓冲区:特点是内存的分配和回收速度快,可以被JVM自动收回;缺点就是如果进行Socket的I/O读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Chanenel中,性能会有一定程度的下降。
(2)直接内存(DirectByteBuf) 字节缓冲区:非堆内存,它在堆外进行内存分配,相比于堆内存,它的分配和回收速度会慢一些,但是将它写入或者从Socket Channel中读取时,由于少了一次内存复制,速度比堆内存快。
正式因为各有利弊,所以Netty提供了多种ByteBuf供开发者使用,经验表明,ByteBuf的最佳实践是在I/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf,这样组合可以达到性能最优。
从内存回收角度看,ByteBuf也可以分为两类:基于对象池的ByteBuf和普通ByteBuf。两者的主要区别就是基于对象池的ByteBuf可以重用ByteBuf对象,它自己维护了一个内存池,可以循环利用创建的ByteBuf,提升内存的使用效率,降低由于高负载导致的频繁GC。测试表名使用内存池后的Netty在高负载、大并发的冲击下内存和GC更加平稳。
到此这篇关于netty中的ByteBuf源码详解的文章就介绍到这了,更多相关netty中的ByteBuf内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!