Netty之ByteBuf应用详解
创始人
2025-05-29 03:03:37

目录

目标

概述

实战

创建直接内存的ByteBuf和堆内存的ByteBuf

创建池化的ByteBuf和非池化的ByteBuf

扩容ByteBuf

ByteBuf写出方法

ByteBuf读入方法

释放ByteBuf的内存

修改ByteBuf

对ByteBuf进行切片(逻辑上的切分)

复制ByteBuf(物理上的)

组合多个ByteBuf


目标

  • 掌握ByteBuf的常用方法。
  • 了解池化的ByteBuf和非池化的ByteBuf的区别。
  • 了解直接内存的ByteBuf和堆内存的ByteBuf的区别。
  • 掌握对ByteBuf的内存释放方法。

概述

池化的ByteBuf和非池化的ByteBuf的区别

类比数据库连接池,池化的ByteBuf可以被重用,对高并发有很好的节约内存的效果。4.1以前的版本默认创建非池化ByteBuf,4.1以后的版本默认创建池化的ByteBuf,但是Android默认创建非池化的ByteBuf。

直接内存的ByteBuf和堆内存的ByteBuf的区别

直接内存的ByteBuf创建和销毁代价大,但是读写性能高,因为少了一次内存复制。减少了垃圾回收机制的压力。

ByteBuf扩容规律

ByteBuf可以指定初始容量也可不指定。不指定初始容量,则容量默认256个字节大小。数据超过容量以后ByteBuf会自动扩容。扩容规则如下:

  • 扩容小于等于512,扩容为16的倍数大小。
  • 扩容大于512,扩容为2的n次方大小。

推荐创建ByteBuf的方法

在Pipeline中创建ByteBuf时推荐使用ChannelHandlerContext,如:

        pipeline.addLast("InboundHandler2", new ChannelInboundHandlerAdapter() {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {ByteBuf buffer = ctx.alloc().buffer(10);}});

实战

创建直接内存的ByteBuf和堆内存的ByteBuf

    /*** 创建基于直接内存和堆内存的ByteBuf* 直接内存创建和销毁代价大,但是读写速度快。*/public void directAndHeap(){//直接内存ByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer();System.out.println(byteBuf.getClass());//堆内存ByteBuf byteBuf1 = ByteBufAllocator.DEFAULT.heapBuffer();System.out.println(byteBuf1.getClass());}

输出结果 


创建池化的ByteBuf和非池化的ByteBuf

第一步:以idea为例,需要做一下配置。

 第二步

 第三步:如图所示,需要将VM options设置为-Dio.netty.allocator.type=unpooled

 第四步:创建ByteBuf并输出ByteBuf类名,此时发现ByteBuf变成了非池化类型。需要注意,ByteBuf默认创建池化类型。


扩容ByteBuf

    /*** ByteBuf扩容案例* ByteBuf可以指定初始容量也可不指定。不指定初始容量,则容量默认256个字节大小。* 数据超过容量以后ByteBuf会自动扩容。扩容规则如下:* 扩容小于等于512,扩容为16的倍数大小。* 扩容大于512,扩容为2的n次方大小。*/public void addCapacity(){ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();System.out.println(buffer);StringBuffer sb = new StringBuffer();//添加257个字节大小的字符串。for(int i=0;i<257;i++){sb.append("a");}buffer.writeBytes(sb.toString().getBytes());System.out.println(buffer);}/*** ByteBuf扩容案例* ByteBuf可以指定初始容量也可不指定。不指定初始容量,则容量默认256个字节大小。* 数据超过容量以后ByteBuf会自动扩容。扩容规则如下:* 扩容小于等于512,扩容为16的倍数大小。* 扩容大于512,扩容为2的n次方大小。*/public void addCapacity2(){ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();System.out.println(buffer);StringBuffer sb = new StringBuffer();//添加513个字节大小的字符串。for(int i=0;i<513;i++){sb.append("a");}buffer.writeBytes(sb.toString().getBytes());System.out.println(buffer);}

ByteBuf写出方法

    public void writeTest() {ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer();//写入byte[]byte[] bytes =new byte[]{'a', 'b', 'c'};byteBuf.writeBytes(bytes);//写入整数byteBuf.writeInt(1);//写入longbyteBuf.writeLong(1L);//写入字符串byteBuf.writeCharSequence("Hello World!",Charset.forName("UTF-8"));//写入StringBufferStringBuffer sb = new StringBuffer();byteBuf.writeCharSequence(sb,Charset.forName("UTF-8"));//写入java.nio.ByteBufferByteBuffer buffer = ByteBuffer.allocate(8);byteBuf.writeBytes(buffer);}

ByteBuf读入方法

    /*** 循环读取ByteBuf,读取过程中移动读指针。*/public void circulateReadTest(){ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer();byteBuf.writeCharSequence("Hello World!",Charset.forName("UTF-8"));//循环读取ByteBuf,观察读指针(ridx)的变化情况。System.out.println(byteBuf);for(int i=byteBuf.readerIndex();i

释放ByteBuf的内存

概述

  • 非池化的堆内存ByteBuf使用JVM内存,由垃圾回收机制回收内存。
  • 非池化的直接内存ByteBuf也可以被垃圾回收机制间接回收内存。
  • 池化的ByteBuf会使用更复杂的内存回收机制回收内存。

ByteBuf实现了io.netty.util.ReferenceCounted接口,采用了引用计数的方法来标记ByteBuf对象的使用情况。其中:

  • release()方法减1,如果计数为0,则ByteBuf被回收。
  • retain()方法加1。

ByteBuf调用release()需要特别注意

第一:并不是在每个Handler的最后调用release(),而是最后一个使用ByteBuf的Handler负责计数减1。因为ByteBuf可能在多个Handler中传递,一旦每个Handler都负责计数减1,则下一个Handler就可能因为ByteBuf的底层内存被回收而无法使用。

第二:虽然Pipeline自带头部和尾部Handler,且会回收ByteBuf的内存,但前提是ByteBuf必须要流转至头部或者尾部,内存才会被回收。比如Handler_1将ByteBuf传递给Handler_2,Handler_2将ByteBuf转成String传递给头部和尾部Handler,则ByteBuf所在内存仍然没有被回收。


源码跟进

尾部Handler对对象计数减1的源码

io.netty.channel.ChannelPipeline接口
io.netty.channel.DefaultChannelPipeline类
io.netty.channel.DefaultChannelPipeline.TailContext类的channelRead(ChannelHandlerContext ctx, Object msg)方法
再跟进到了onUnhandledInboundMessage(Object msg)方法,该方法的内部有ReferenceCountUtil.release(msg)负责计数减1
进入release(msg),发现对象必须要实现io.netty.util.ReferenceCounted接口,对象计数才会被减1

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

头部Handler对对象计数减1的源码

io.netty.channel.ChannelPipeline接口
io.netty.channel.DefaultChannelPipeline类
io.netty.channel.DefaultChannelPipeline.HeadContext类的write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)方法
再跟进到了io.netty.channel.AbstractChannel类的write(Object msg, ChannelPromise promise)方法,当没有出栈缓存时进入计数减1操作,同样地,进入release(msg)后发现对象必须要实现io.netty.util.ReferenceCounted接口,对象计数才会被减1

    public final void write(Object msg, ChannelPromise promise) {this.assertEventLoop();ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;if (outboundBuffer == null) {try {ReferenceCountUtil.release(msg);} finally {this.safeSetFailure(promise, this.newClosedChannelException(AbstractChannel.this.initialCloseCause, "write(Object, ChannelPromise)"));}} else {int size;try {msg = AbstractChannel.this.filterOutboundMessage(msg);size = AbstractChannel.this.pipeline.estimatorHandle().size(msg);if (size < 0) {size = 0;}} catch (Throwable var15) {try {ReferenceCountUtil.release(msg);} finally {this.safeSetFailure(promise, var15);}return;}outboundBuffer.addMessage(msg, size, promise);}}public static boolean release(Object msg) {return msg instanceof ReferenceCounted ? ((ReferenceCounted)msg).release() : false;}

修改ByteBuf

    /*** 根据下标修改ByteBuf元素*/public void updateTest(){//创建一个初始容量为10字节大小的ByteBufByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer(10);byteBuf.writeBytes(new byte[]{'1','2','3','4','5','6','7','8','9','0'});//修改下标为0的元素为'2'byteBuf.setByte(0,'2');for(int i=0;i

对ByteBuf进行切片(逻辑上的切分)

    /*** 切分ByteBuf*/public void sliceTest(){//创建一个初始容量为10字节大小的ByteBufByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer(10);byteBuf.writeBytes(new byte[]{'1','2','3','4','5','6','7','8','9','0'});//从下标2开始切分,切取5个长度。ByteBuf sliceByteBuf = byteBuf.slice(2, 5);for(int i=0;i

特别注意

  • 切片后的ByteBuf不能扩容。
  • 如果原始的ByteBuf内存被释放,则切片后的ByteBuf不能使用。所以切片后的ByteBuf需要将计数加一。

推荐案例

    public void sliceTest(){//创建一个初始容量为10字节大小的ByteBufByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer(10);byteBuf.writeBytes(new byte[]{'1','2','3','4','5','6','7','8','9','0'});//从下标2开始切分,切取5个长度。ByteBuf sliceByteBuf = byteBuf.slice(2, 5);//引用计数加一。sliceByteBuf.retain();//将原始的ByteBuf释放掉。byteBuf.release();//发现原来的ByteBuf还可以继续使用。for(int i=0;i

复制ByteBuf(物理上的)

    /***copy()方法对ByteBuf进行了深拷贝,是物理上的复制,与原来的ByteBuf无关联。*/public void copyTest(){//创建一个初始容量为10字节大小的ByteBufByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer(10);byteBuf.writeBytes(new byte[]{'1','2','3','4','5','6','7','8','9','0'});//复制整个原始的ByteBuf。ByteBuf cp =byteBuf.copy();cp.setByte(0,'b');for(int i=0;i

组合多个ByteBuf

    /*** 组合多个ByteBuf* 方法1:通过writeBytes方法组合多个ByteBuf。新组合的ByteBuf与之前的ByteBuf完全独立开。*/public void writeBytesTest(){ByteBuf byteBuf = ByteBufAllocator.DEFAULT.directBuffer(5);byteBuf.writeBytes(new byte[]{'1','2','3','4','5'});ByteBuf byteBuf2 = ByteBufAllocator.DEFAULT.directBuffer(5);byteBuf2.writeBytes(new byte[]{'6','7','8','9','0'});//ByteBuf byteBuf3 = ByteBufAllocator.DEFAULT.directBuffer(10);byteBuf3.writeBytes(byteBuf).writeBytes(byteBuf2);for(int i=0;i

相关内容

热门资讯

玛雅人的五大预言 玛雅人预言2... 曾经玛雅人预言2012年是世界末日,但当时好像没有发生什么。没想到10年后的2022年,疫情,战争,...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
荼蘼什么意思 岁月缱绻葳蕤生香... 感谢作者【辰夕】的原创独家授权分享编辑整理:【多肉植物百科】百科君坐标:云南 曲靖春而至,季节流转,...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...