在Netty中,还有另外一个比拟常见的对象ByteBuf,它其实等同于Java Nio中的ByteBuffer,然而ByteBuf对Nio中的ByteBuffer的性能做了很作加强,上面咱们来简略理解一下ByteBuf。
上面这段代码演示了ByteBuf的创立以及内容的打印,这里显示出了和一般ByteBuffer最大的区别之一,就是ByteBuf能够主动扩容,默认长度是256,如果内容长度超过阈值时,会主动触发扩容
public class ByteBufExample { public static void main(String[] args) { ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可主动扩容 log(buf); StringBuilder sb=new StringBuilder(); for (int i = 0; i < 32; i++) { //演示的时候,能够把循环的值扩充,就能看到扩容成果 sb.append(" - "+i); } buf.writeBytes(sb.toString().getBytes()); log(buf); } private static void log(ByteBuf buf){ StringBuilder builder=new StringBuilder() .append(" read index:").append(buf.readerIndex()) //获取读索引 .append(" write index:").append(buf.writerIndex()) //获取写索引 .append(" capacity:").append(buf.capacity()) //获取容量 .append(StringUtil.NEWLINE); //把ByteBuf中的内容,dump到StringBuilder中 ByteBufUtil.appendPrettyHexDump(builder,buf); System.out.println(builder.toString()); }}
ByteBuf创立的办法有两种
第一种,创立基于堆内存的ByteBuf
ByteBuf buffer=ByteBufAllocator.DEFAULT.heapBuffer(10);
第二种,创立基于间接内存(堆外内存)的ByteBuf(默认状况下用的是这种)
Java中的内存分为两个局部,一部分是不须要jvm治理的间接内存,也被称为堆外内存。堆外内存就是把内存对象调配在JVM堆意外的内存区域,这部分内存不是虚拟机治理,而是由操作系统来治理,这样能够缩小垃圾回收对应用程序的影响
ByteBufAllocator.DEFAULT.directBuffer(10);
间接内存的益处是读写性能会高一些,如果数据寄存在堆中,此时须要把Java堆空间的数据发送到近程服务器,首先须要把堆外部的数据拷贝到间接内存(堆外内存),而后再发送。如果是把数据间接存储到堆外内存中,发送的时候就少了一个复制步骤。
然而它也有毛病,因为短少了JVM的内存治理,所以须要咱们本人来保护堆外内存,避免内存溢出。
另外,须要留神的是,ByteBuf默认采纳了池化技术来创立。对于池化技术在后面的课程中曾经反复讲过,它的核心思想是实现对象的复用,从而缩小对象频繁创立销毁带来的性能开销。
池化性能是否开启,能够通过上面的环境变量来管制,其中unpooled示意不开启。
-Dio.netty.allocator.type={unpooled|pooled}
public class NettyByteBufExample { public static void main(String[] args) { ByteBuf buf= ByteBufAllocator.DEFAULT.buffer(); System.out.println(buf); }}
ByteBuf的存储构造
ByteBuf的存储构造如图3-1所示,从这个图中能够看到ByteBuf其实是一个字节容器,该容器中蕴含三个局部
- 曾经抛弃的字节,这部分数据是有效的
- 可读字节,这部分数据是ByteBuf的主体数据,从ByteBuf外面读取的数据都来自这部分; 可写字节,所有写到ByteBuf的数据都会存储到这一段
- 可扩容字节,示意ByteBuf最多还能扩容多少容量。
<center>图3-1</center>
在ByteBuf中,有两个指针
- readerIndex: 读指针,每读取一个字节,readerIndex自减少1。ByteBuf外面总共有witeIndex-readerIndex个字节可读,当readerIndex和writeIndex相等的时候,ByteBuf不可读
- writeIndex: 写指针,每写入一个字节,writeIndex自减少1,直到减少到capacity后,能够触发扩容后持续写入。
- ByteBuf中还有一个maxCapacity最大容量,默认的值是
Integer.MAX_VALUE
,当ByteBuf写入数据时,如果容量有余时,会触发扩容,直到capacity扩容到maxCapacity。
ByteBuf中罕用的办法
对于ByteBuf来说,常见的办法就是写入和读取
Write相干办法
对于write办法来说,ByteBuf提供了针对各种不同数据类型的写入,比方
- writeChar,写入char类型
- writeInt,写入int类型
- writeFloat,写入float类型
- writeBytes, 写入nio的ByteBuffer
- writeCharSequence, 写入字符串
public class ByteBufExample { public static void main(String[] args) { ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可主动扩容 buf.writeBytes(new byte[]{1,2,3,4}); //写入四个字节 log(buf); buf.writeInt(5); //写入一个int类型,也是4个字节 log(buf); } private static void log(ByteBuf buf){ System.out.println(buf); StringBuilder builder=new StringBuilder() .append(" read index:").append(buf.readerIndex()) .append(" write index:").append(buf.writerIndex()) .append(" capacity:").append(buf.capacity()) .append(StringUtil.NEWLINE); //把ByteBuf中的内容,dump到StringBuilder中 ByteBufUtil.appendPrettyHexDump(builder,buf); System.out.println(builder.toString()); }}
扩容
当向ByteBuf写入数据时,发现容量有余时,会触发扩容,而具体的扩容规定是
假如ByteBuf初始容量是10。
- 如果写入后数据大小未超过512个字节,则抉择下一个16的整数倍进行库容。 比方写入数据后大小为12,则扩容后的capacity是16。
- 如果写入后数据大小超过512个字节,则抉择下一个2^n^。 比方写入后大小是512字节,则扩容后的capacity是2^10^=1024 。(因为2^9^=512,长度曾经不够了)
- 扩容不能超过max capacity,否则会报错。
Reader相干办法
reader办法也同样针对不同数据类型提供了不同的操作方法,
- readByte ,读取单个字节
- readInt , 读取一个int类型
- readFloat ,读取一个float类型
public class ByteBufExample { public static void main(String[] args) { ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可主动扩容 buf.writeBytes(new byte[]{1,2,3,4}); log(buf); System.out.println(buf.readByte()); log(buf); } private static void log(ByteBuf buf){ StringBuilder builder=new StringBuilder() .append(" read index:").append(buf.readerIndex()) .append(" write index:").append(buf.writerIndex()) .append(" capacity:").append(buf.capacity()) .append(StringUtil.NEWLINE); //把ByteBuf中的内容,dump到StringBuilder中 ByteBufUtil.appendPrettyHexDump(builder,buf); System.out.println(builder.toString()); }}
从上面后果中能够看到,读完一个字节后,这个字节就变成了废除局部,再次读取的时候只能读取 未读取的局部数据。
read index:0 write index:7 capacity:256 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 01 02 03 04 05 06 07 |....... |+--------+-------------------------------------------------+----------------+1 read index:1 write index:7 capacity:256 +-------------------------------------------------+ | 0 1 2 3 4 5 6 7 8 9 a b c d e f |+--------+-------------------------------------------------+----------------+|00000000| 02 03 04 05 06 07 |...... |+--------+-------------------------------------------------+----------------+Process finished with exit code 0
另外,如果想反复读取哪些曾经读完的数据,这里提供了两个办法来实现标记和重置
public static void main(String[] args) { ByteBuf buf= ByteBufAllocator.DEFAULT.heapBuffer();//可主动扩容 buf.writeBytes(new byte[]{1,2,3,4,5,6,7}); log(buf); buf.markReaderIndex(); //标记读取的索引地位 System.out.println(buf.readInt()); log(buf); buf.resetReaderIndex();//重置到标记位 System.out.println(buf.readInt()); log(buf);}
另外,如果想不扭转读指针地位来取得数据,在ByteBuf中提供了get
结尾的办法,这个办法基于索引地位读取,并且容许反复读取的性能。
ByteBuf的零拷贝机制
须要阐明一下,ByteBuf的零拷贝机制和咱们之前提到的操作系统层面的零拷贝不同,操作系统层面的零拷贝,是咱们要把一个文件发送到近程服务器时,须要从内核空间拷贝到用户空间,再从用户空间拷贝到内核空间的网卡缓冲区发送,导致拷贝次数减少。
而ByteBuf中的零拷贝思维也是雷同,都是缩小数据复制晋升性能。如图3-2所示,假如有一个原始ByteBuf,咱们想对这个ByteBuf其中的两个局部的数据进行操作。依照失常的思路,咱们会创立两个新的ByteBuf,而后把原始ByteBuf中的局部数据拷贝到两个新的ByteBuf中,然而这种会波及到数据拷贝,在并发量较大的状况下,会影响到性能。
<center>图3-2</center>
ByteBuf中提供了一个slice办法,这个办法能够在不做数据拷贝的状况下对原始ByteBuf进行拆分,应用办法如下
public static void main(String[] args) { ByteBuf buf= ByteBufAllocator.DEFAULT.buffer();//可主动扩容 buf.writeBytes(new byte[]{1,2,3,4,5,6,7,8,9,10}); log(buf); ByteBuf bb1=buf.slice(0,5); ByteBuf bb2=buf.slice(5,5); log(bb1); log(bb2); System.out.println("批改原始数据"); buf.setByte(2, 5); //批改原始buf数据 log(bb1);//再打印bb1的后果,发现数据产生了变动}
在下面的代码中,通过slice对原始buf进行切片,每个分片是5个字节。
为了证实slice是没有数据拷贝,咱们通过批改原始buf的索引2所在的值,而后再打印第一个分片bb1,能够发现bb1的后果产生了变动。阐明两个分片和原始buf指向的数据是同一个。
Unpooled
在后面的案例中咱们常常用到Unpooled工具类,它是同了非池化的ByteBuf的创立、组合、复制等操作。
假如有一个协定数据,它有头部和音讯体组成,这两个局部别离放在两个ByteBuf中
ByteBuf header=...ByteBuf body= ...
咱们心愿把header和body合并成一个ByteBuf,通常的做法是
ByteBuf allBuf=Unpooled.buffer(header.readableBytes()+body.readableBytes());allBuf.writeBytes(header);allBuf.writeBytes(body);
在这个过程中,咱们把header和body拷贝到了新的allBuf中,这个过程在无形中减少了两次数据拷贝操作。那有没有更高效的办法缩小拷贝次数来达到雷同目标呢?
在Netty中,提供了一个CompositeByteBuf组件,它提供了这个性能。
public class ByteBufExample { public static void main(String[] args) { ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可主动扩容 header.writeCharSequence("header", CharsetUtil.UTF_8); ByteBuf body=ByteBufAllocator.DEFAULT.buffer(); body.writeCharSequence("body", CharsetUtil.UTF_8); CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer(); //其中第一个参数是 true, 示意当增加新的 ByteBuf 时, 主动递增 CompositeByteBuf 的 writeIndex. //默认是false,也就是writeIndex=0,这样的话咱们不可能从compositeByteBuf中读取到数据。 compositeByteBuf.addComponents(true,header,body); log(compositeByteBuf); } private static void log(ByteBuf buf){ StringBuilder builder=new StringBuilder() .append(" read index:").append(buf.readerIndex()) .append(" write index:").append(buf.writerIndex()) .append(" capacity:").append(buf.capacity()) .append(StringUtil.NEWLINE); //把ByteBuf中的内容,dump到StringBuilder中 ByteBufUtil.appendPrettyHexDump(builder,buf); System.out.println(builder.toString()); }}
之所以CompositeByteBuf可能实现零拷贝,是因为在组合header和body时,并没有对这两个数据进行复制,而是通过CompositeByteBuf构建了一个逻辑整体,外面依然是两个实在对象,也就是有一个指针指向了同一个对象,所以这里相似于浅拷贝的实现。
wrappedBuffer
在Unpooled工具类中,提供了一个wrappedBuffer办法,来实现CompositeByteBuf零拷贝性能。应用办法如下。
public static void main(String[] args) { ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可主动扩容 header.writeCharSequence("header", CharsetUtil.UTF_8); ByteBuf body=ByteBufAllocator.DEFAULT.buffer(); body.writeCharSequence("body", CharsetUtil.UTF_8); ByteBuf allBb=Unpooled.wrappedBuffer(header,body); log(allBb); //对于零拷贝机制,批改原始ByteBuf中的值,会影响到allBb header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8); log(allBb); }
copiedBuffer
copiedBuffer,和wrappedBuffer最大的区别是,该办法会实现数据复制,上面代码演示了copiedBuffer和wrappedbuffer的区别,能够看到在case
标注的地位中,批改了原始ByteBuf的值,并没有影响到allBb。
public static void main(String[] args) { ByteBuf header= ByteBufAllocator.DEFAULT.buffer();//可主动扩容 header.writeCharSequence("header", CharsetUtil.UTF_8); ByteBuf body=ByteBufAllocator.DEFAULT.buffer(); body.writeCharSequence("body", CharsetUtil.UTF_8); ByteBuf allBb=Unpooled.copiedBuffer(header,body); log(allBb); header.setCharSequence(0,"Newer0",CharsetUtil.UTF_8); //case log(allBb);}
内存开释
针对不同的ByteBuf创立,内存开释的办法不同。
- UnpooledHeapByteBuf,应用JVM内存,只须要期待GC回收即可
- UnpooledDirectByteBuf,应用对外内存,须要非凡办法来回收内存
- PooledByteBuf和它的之类应用了池化机制,须要更简单的规定来回收内存
如果ByteBuf是应用堆外内存来创立,那么尽量手动开释内存,那怎么开释呢?
Netty采纳了援用计数办法来管制内存回收,每个ByteBuf都实现了ReferenceCounted接口。
- 每个ByteBuf对象的初始计数为1
- 调用release办法时,计数器减一,如果计数器为0,ByteBuf被回收
- 调用retain办法时,计数器加一,示意调用者没用完之前,其余handler即时调用了release也不会造成回收。
- 当计数器为0时,底层内存会被回收,这时即便ByteBuf对象还存在,然而它的各个办法都无奈失常应用
版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自Mic带你学架构
!
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!