Netty组件之ByteBuf详解 源码分析

2023-11-05 09:59

本文主要是介绍Netty组件之ByteBuf详解 源码分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

学习了前面的一些netty组件,此篇将讲解最后一个组件ByteBuf,ByteBuf是对Nio的ByteBuffer的一个增强。

1.创建ByteBuf对象

ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();

这是最基本的创建方式,我们也可指定其初始容量和最大容量(可扩容)。

首先概要看看源码,有一个大致的了解

点进我们上面的buffer方法,源码的关键代码如下

 继续查看AbstractByteBufAllocator子实现抽象类,对应三个buffer方法的重载并判断需要创建直接内存或是堆内存,和NIO的ByteBuffer雷同:

 继续查看分配的一些细节,点入分配的方法,可以看到一些初始值

进入关键方法,newHeaoBuffer方法,如下,是两个抽象方法,我们可以看到下面的两个实现类

2.直接内存vs堆内存

分配两种不同空间的方式如下,底层就是上面看过的源码。其中堆内存的空间分配空间效率较快但是读写效率较高,直接内存相反,如果对Nio不了解的可以看看Nio的零拷贝那一篇。

ByteBuf buf = ByteBufAllocator.DEFAULT.directBuffer();ByteBufAllocator.DEFAULT.heapBuffer();
  • 直接内存创建和销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能一起用
  • 直接内存对GC压力小,因为这部分内存不受JVM垃圾回收的管理,但也要注意及时主动释放

如果使用buffer方法默认分配的直接内存

3.池化vs非池化

这里池化的思想和数据库连接池和线程池一个思想,netty的ByteBuf也支持这一种的池化

池化的最大意义在于可以重用 ByteBuf,优点有

  • 没有池化、则每次都需要创建新的ByteBuf实例,这个操作对直接内存代价昂贵,就算是堆内存,内存也会,也会增加GC的压力
  • 有了池化,则可以重用池中 ByteBuf实例,并且采用了与jemalloc类似的内存分配算法提升分配效率
  • 高并发时,池化功能更节约内存,减少内存溢出的可能

池化功能是否开启,可以通过下面的系统环境变量来设置(默认开启)

  • 4.1以后,非 Android平台默认启用池化实现,Android平台启用非池化实现
  • 4.1之前,池化功能还不成熟,默认是非池化实现

查看源码:

        我们创建ByteBuf时,调用的是该接口的一个成员变量,它是ByteBufUtil类中的类变量,类型依然是ByteBufAllocator

         进入ByteBufUtil找到该静态变量,并查看该类中的静态代码块

 这也就验证了我们可以使用-Dio.netty.allocator.type={unpooled|pooled}来设置是否池化ByteBuf,

-D的全称是defintion,就是定义的意思,差不多应该表达的就是用户自定义参数的意思。必须加-D

需要查看当前的ByteBuf是什么类型的,只需要打印该对象的.getClass()查看是哪个类创建的即可,带pooled的即池化,带direct的即分配直接内存。如class UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf

4.组成

ByteBuf由四部分组成

 也就是一边写一边读,读到后面前面的数据也就没用了,当写满后还会动态扩容,最大扩容大小可以自己设定,默认为int的最大值。此部分和ByteBuffer的区别显而易见,有两个指针,所以不需要切换读写模式,方便使用。

5.写入

方法名含义备注
writeInt(int value)写入int值Big Endian,大端写入,即0x250,写入后 00 00 02 50
writeIntLE(int value)写入int值Little Endian,小端写入,即0x250,写入后 50 02 00 00
writeBoolean(int value)写入boolean值用一字节01|00代表true|false
writeByte(int value)写入byte值
writeBytes(byte[] src)写入byte[]
writeBytes(ByteBuf src)写入netty的ByteBuf
writeBytes(ByteBuffer src)写入nio的ByteBuf

int writeCharSequence(CharSequence

sequence,Charset charset)

写入字符串
  • 这些方法的未指明返回值的,其返回值都是 ByteBuf,意味着可以链式调用
  • 网络传输默认习惯是 Big Endian

小端存储指从内存的低地址开始,先存储数据的低序字节再存高序字节;相反,大端存储指从内存的低地址开始,先存储数据的高序字节再存储数据的低序字节。

 还有一类方法是set开头的一系列方法,也可以写入数据,但不会改变写指针位置

6.扩容

  • 如果写入后数据大小未超过512,则选择下一个16的整数倍,例如写入后大小为12,则扩容后capacity是16
  • 如果写入后数据大小超过512,则选择下一个2^n,例如写入后大小为513,则扩容后capacity是2^10=1024 (2^9=512已经不够了)
  • 扩容不能超过max capacity会报错

7.读取

和write相对应

 读过的内容,就属于废弃部分了,再读只能读那些尚未读取的部分

如果需要重复读取int整数5,怎么办?
可以在read前先做个标记mark

buf.markReaderIndex();//做一个读标记
buf.resetReaderIndex();//回到标记

除了做标记,还可以通过一个个的索引get()值

8.retain&release

由于Netty中有堆外内存的ByteBuf 实现,堆外内存最好是手动来释放,而不是等GC垃圾回收。

  • UnpooledHeapByteBuf使用的是JVM内存,只需等GC回收内存即可
  • UnpooledDirectByteBuf使用的就是直接内存了,需要特殊的方法来回收内存
  • PooledByteBuf和它的子类使用了池化机制,需要更复杂的规则来回收内存
     

Netty这里采用了引用计数法来控制回收内存,每个ByteBuf都实现了ReferenceCounted引用计数法接口

  • 每个ByteBuf对象的初始计数为1
  • 调用release方法计数减1,如果计数为0,ByteBuf内存被回收
  • 调用retain方法计数加1,表示调用者没用完之前,其它handler即使调用了release 也不会造成回收
  • 当计数为0时,底层内存会被回收,这时即使ByteBuf对象还在,其各个方法均无法正常使用
     

源码如下:

 ReferenceCounted接口,具体的实现针对不同的ByteBuf实现

 谁来负责release呢?

我们都知道ByteBuf存在在一个管道里互相传递,当有哪一个handler中不在需要传递该ByteBuf时,就可以销毁了。

想象这么一个情景,我们读取到ByteBuf在h1Hadnler里把该ByteBuf转为了String,h1把String传递给h2,而不是ByteBuf,此时为了能给及时的释放,h1就应该释放此ByteBuf的分配的空间;如果h1把ByteBuf传给h2,h2继续传下去给tail,则已经到尾部,ByteBuf已经没用了,需要由tail释放。

基本规则是,谁是最后使用者,谁负责release,详细分析如下

TailContext分析

找到一个名为TailContext的类,此类就是我们最后的tail节点,如下

 因为它实现了ChannelInboundHandler,所以可以找到它实现的channelRead方法,如下:

深入查看,可以看到

可以看到,如果msg是一个ReferenceCounted实现的话,Tail就会调用该该对象的release方法。 

HeadContext分析

 头结点不仅实现了Inboud还实现了outbound,因为HeadContex也需要做一个收尾动作,Netty 组件 Channel 、Future 、Promise_清风拂来水波不兴的博客-CSDN博客

前面说过write事件是从后往前传递,从tail开始,一直到head,head当然要留意write事件了

9.slice

【零拷贝】的体现之一,对原始ByteBuf进行切片成多个ByteBuf,切片后的ByteBuf并没有发生内存复制,还是使用原始ByteBuf的内存,切片后的 ByteBuf维护独立的read,write指针

ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(10);
buf.writeBytes(new byte[]{'a','b','c','d','e','f'});
// slice(int index, int length)从index切length个,返回一个新的ByteBuf对象
ByteBuf slice = buf.slice(0,5);
  • 也就是这个新的对象和以前的公用同一段内存,任意一个改了的话其他的也会改;
  • 切片后的新ByteBuf容量不能增加,因为增加的话再写数据就乱套了
  • 如果切片后的ByteBuf进行了release的话,就不能再使用了,所以一般分片后都会给原始的ByteBuf进行retain,防止被回收,导致切片的使用出异常,但一定要记得release,否则会导致幽灵空间,也就是内存泄漏

10.deplicate

【零拷贝】的体现之一,就好比截取了原始ByteBuf所有内容,并且没有max capacity的限制,也是与原始ByteBuf使用同一块底层内存,只是读写指针是独立的

11.copy

会将底层内存数据进行深拷贝,因此无论读写,都与原始 ByteBuf 无关

12.composite

把多个小的ByteBuf组合成一个大的ByteBuf,也没用复制操作,但是却带来了更复杂的维护,因为这只是逻辑上的合并

ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(5);
ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf.writeBytes(new byte[]{1,2,3,4,5});
buf1.writeBytes(new byte[]{6,7,8,9,10});
//创建CompositeByteBuf对象
CompositeByteBuf buffer = ByteBufAllocator.DEFAULT.compositeBuffer();
//addComponents(boolean increaseWriterIndex,ByteBuf... buffers),布尔值表示组合后读写指针要更新,这是个小小的坑
buffer.addComponents(true,buf, buf1);
System.out.println(buffer);

13.UnPooled

Unpooled是一个工具类,类如其名,提供了非池化的ByteBuf创建、组合、复制等操作这里仅介绍其跟【零拷贝】相关的wrappedBuffer方法,可以用来包装ByteBuf
 

ByteBuf buf1 = ByteBufAllocator.DEFAULT.buffer(5);
buf1.writeBytes(new byte[]{1, 2, 3, 4, 5});
ByteBuf buf2 = ByteBufAllocator.DEFAULT.buffer(5);
buf2.writeBytes(new byte[]{6,7,8,9,10});
//当包装ByteBuf个数超过一个时,底层使用了CompositeByteBuf
ByteBuf buf3 = Unpooled.wrappedBuffer(buf1, buf2);
System.out.println(ByteBufUtil.prettyHexDump(buf3));

输出

 

这篇关于Netty组件之ByteBuf详解 源码分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/349095

相关文章

MySQL数据库双机热备的配置方法详解

《MySQL数据库双机热备的配置方法详解》在企业级应用中,数据库的高可用性和数据的安全性是至关重要的,MySQL作为最流行的开源关系型数据库管理系统之一,提供了多种方式来实现高可用性,其中双机热备(M... 目录1. 环境准备1.1 安装mysql1.2 配置MySQL1.2.1 主服务器配置1.2.2 从

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

MyBatis常用XML语法详解

《MyBatis常用XML语法详解》文章介绍了MyBatis常用XML语法,包括结果映射、查询语句、插入语句、更新语句、删除语句、动态SQL标签以及ehcache.xml文件的使用,感兴趣的朋友跟随小... 目录1、定义结果映射2、查询语句3、插入语句4、更新语句5、删除语句6、动态 SQL 标签7、ehc

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

k8s按需创建PV和使用PVC详解

《k8s按需创建PV和使用PVC详解》Kubernetes中,PV和PVC用于管理持久存储,StorageClass实现动态PV分配,PVC声明存储需求并绑定PV,通过kubectl验证状态,注意回收... 目录1.按需创建 PV(使用 StorageClass)创建 StorageClass2.创建 PV

Python版本信息获取方法详解与实战

《Python版本信息获取方法详解与实战》在Python开发中,获取Python版本号是调试、兼容性检查和版本控制的重要基础操作,本文详细介绍了如何使用sys和platform模块获取Python的主... 目录1. python版本号获取基础2. 使用sys模块获取版本信息2.1 sys模块概述2.1.1

一文详解Python如何开发游戏

《一文详解Python如何开发游戏》Python是一种非常流行的编程语言,也可以用来开发游戏模组,:本文主要介绍Python如何开发游戏的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录一、python简介二、Python 开发 2D 游戏的优劣势优势缺点三、Python 开发 3D

Redis 基本数据类型和使用详解

《Redis基本数据类型和使用详解》String是Redis最基本的数据类型,一个键对应一个值,它的功能十分强大,可以存储字符串、整数、浮点数等多种数据格式,本文给大家介绍Redis基本数据类型和... 目录一、Redis 入门介绍二、Redis 的五大基本数据类型2.1 String 类型2.2 Hash