当前位置:首页 » 操作系统 » netty架构源码

netty架构源码

发布时间: 2022-11-30 12:25:12

1. Netty WebSocket 拆包浅析

最近项目中运用 WebSocket 的场景越来越多,自然而然。踩到的坑也就越来越多,例如最经常遇到的拆包,粘包问题。而当发现这个问题的时候,起初我认为与我之前通过 mina  实现自定义协议头,解决 socket 进行数据传输时遇到拆包问题的场景是一样的。而当我慢慢深入了解 WebSocket的时候才发现,实际上,WebSocket 的出现,就是为了解决拆包,粘包的问题的。因为这些事情,本身就是应该由运用层去解决,而不是在TCP/IP层解决。

通过网络搜索,我们可以知道 WebSocket 协议是根据 RFC6455 规范实现的,而最新版本是13(距离现今也接近10年了)。

以下是 WebScoket 定义的协议帧片段。

通过以上的帧定义片段。我也就明白了,为什么说 WebSocket 是为了解决拆包,粘包问题等一系列应用层问题而诞生的。从 RCF6455 规范定义第 5.3 章节中可以发现,每次发送或者接收的帧报文中,都会有定义协议的一些信息,例如头部大小,数据大小,以及帧类型,标识位等信息。

看完协议定义,那么开始查看  Netty  源码

以下是  Netty  定义的相关帧类型以供开发者使用。

当服务端接收到协议由 HTTP 握手升级协议 WebSocket 时,我们需要通过  类新建 WebSocketHandshaker ,此时会判断 WebSocket 协议版本以及相关信息进行校验。

而  WebSocketServerHandshaker 中,分别定义了  WebSocketFrameDecoder , WebSocketFrameEncoder 进行解码,编码调用。

那么从现实开始,大概也就清楚了, Netty  中  WebSocket  和  Socket  实现,实际上都是一样的,都是一个解码器(负责接收数据,处理成需要的类型,例如文本,二进制),一个编码器(负责根据协议版本,进行帧封装)的结构。

从  WebSocket13FrameDecoder  我们可以得知,实际上  WebSocket13FrameDecoder 是继承自  WebSocket08FrameDecoder (由于实现细节一致),而  WebSocket08FrameDecoder 是继承自  ByteToMessageDecoder  ,实际上都是通过二进制数据进行解码处理的。

查看  WebSocket08FrameDecoder  我们可以了解到具体的实现细节,就是每次解码的时候,都会读取首部信息,然后依次对数据进行处理。而拆包等操作,都是已经进行的相应处理和封装。

看完 Netty 的源码实现,那么就可以进行实际的编码解决问题了。

当服务端/客户端发现包文过大时,会进行拆包。而为每个包定义一系列的定义。

例如:当接收一个 Text 消息时, Netty 首先会实例化一个 TextWebSocketFrame 对象并传递给调用方,而通过  isFinalFragment 我们可以判断出,这个帧对象是否已经传输完毕,如果传输完毕,那么进行业务处理。如果没有传输完毕,那么继续等待余下信息,进行拼接处理。

以上为服务端接收拆包信息的处理方式,反之亦然,客户端接收消息也可以进行相应操作。而根据 WebSocket 1.3 版本实现的组件,也都根据 RFC6455  规范进行相应实现,可以实现无缝对接。

参考: Netty做webSocket客户端,服务端拆包发送客户端接收处理

2. netty里的ByteBuf扩容源码分析

  我们知道在实例化一个ByteBuf对象的时候,是可以设置一个capacity和一个maxCapacity,当writerIndex达到capacity的时候,再往里面写入内容,ByteBuf就会进行扩容。
  下面我们来看一些ByteBuf是怎样进行扩容的:
  我们先看一下调用ByteBuf的writeByte(int value)写入一个字节的数据。因为ByteBuf的writeByte(int value)是一个抽象方法,它的具体实现是在AbstractByteBuf里面

  我们能看到会先调用ensureWritable0()方法来检查是否能往里面写入数据,传入1

  ensureAccessible()方法是检查refCnt是否为0,为0代表该ByteBuf对象以被释放,会抛。
  接下来判断minWritableBytes是否小于或等于writableBytes,如果满足代表还有空间满足写入数据,则直接返回。如果不满足,则判断是否检查边界,checkBounds是个boolean值,可在启动JVM的时候由io.netty.buffer.checkBounds参数指定,其默认值是true。如果checkBounds为true,判断将要写入的字节数是否大于最大可写入的字节数(maxCapacity - writerIndex),如果大于直接抛异常,否则继续执行。
  接下来会调用ByteBufAllocator的calculateNewCapacity计算新的capacity。方法的实现在AbstractByteBufAllocator里面:

  我们能看到扩容是有一个阀值(CALCULATE_THRESHOLD)的,为4MB大小,当所需容量大小(minNewCapacity)小于阀值(threshold)的时候,新的容量(newCapacity)都是是以64位基数向坐移位位计算出来的,通过循环,每次移动移1位,直到newCapacity>=minNewCapacity为止,如果计算出来newCapacity大于maxCapacity,则返回maxCapacity,否则返回newCapacity。也就是说当minNewCapacity=300的时候,newCapacity=512。
  当minNewCapacity>=threshold的时候,则先计算minNewCapacity / threshold * threshold的大小,如果这个值在加上一个threshold(4MB)大于newCapacity的时候,则newCapacity的值取maxCapacity,否则newCapacity=minNewCapacity / threshold * threshold+threshold。
  再回到ensureWritable0()方法,我们能看到,拿到计算出来的newCapacity,原后调用capacity(newCapacity)方法将新的capacity设置进去。

3. 如何实现Netty框架中服务器端的消息推送

netty框架是用在服务器端,客户端是嵌入式编程,通过自定义的tcp通信协议进行连接的,现在需求是这样的,服务器端只是用来和客户端进行通信,现在有第三方如微信端进行支付成功后在数据库里生成了一条数据,表示要往某个客户端发送指令,以下两种方式可供参考:
1、微信端生成通讯指令后调用TCP端的接口(负责通讯程序和数据库交互的),在接口程序中通过定义Socket连到通讯程序服务器端,根据通道编号去发送,但是这种会导致服务器端的tcp客户端连接变得更多。

2、直接在netty框架中定义了scheleAtF。
当然也可借助第三方工具来完成推送。例如极光推送,极光推送具有以下功能:
1、多种消息类型
开发者可以轻松地通过极光发送各个移动平台的系统通知,还可以在控制台编辑多种富文本展示模板; 极光还提供自定义消息的透传,客户端接到消息内容后根据自己的逻辑自由处理。
2、用户和推送统计
完整的消息生命周期查询,并且可以形成“推送报表”与“用户统计报表”呈现给开发者,用来观察推送的效果和应用发展趋势。
3、短信补充
通过极光后台推送APP通知消息,对于一些重要又不能遗漏的信息可以调用极光短信的后台对未收到的客户端发送短信通知,保证消息的可靠性。
4、A/B 测试
合理的推送能够激活用户,提高用户粘性,使用A/B分组测试的科学方法,根据测试反馈的结果,帮助开发者选择最优化的推送方案。
5、极光推送安全包
为金融、新闻、政务及其他对推送安全要求极高的客户提供安全严谨、稳定可靠的信息推送解决方案
6、可定制的私有云
对于安全性要求更高,希望推送数据和系统存储在自己服务器的客户,及个性化需求需要定制开发的,性能更高要求的,或者想拥有自己推送平台的甚至要求源码授权二次开发的开发者,极光提供全功能的私有云解决方案。
深圳市和讯华谷信息技术有限公司(极光 Aurora Mobile,纳斯达克股票代码:JG)成立于2011年,是中国领先的开发者服务提供商,专注于为开发者提供稳定高效的消息推送、一键认证以及流量变现等服务,助力开发者的运营、增长与变现。同时,极光的行业应用已经拓展至市场洞察、金融风控与商业地理服务,助力各行各业优化决策、提升效率。

4. Netty源码篇-处理OP_ACCEPT

AbstractBootstrap#initAndRegister

MultithreadEventLoopGroup#register

MultithreadEventExecutorGroup#next

AbstractChannel#AbstractUnsafe#register

SingleThreadEventExecutor#execute

SingleThreadEventExecutor#startThread

SingleThreadEventExecutor#doStartThread

如果有客户端连接,channel会利用unsafe去读取数据

NioEventLoop#processSelectedKey

AbstractNioMessageChannel#NioMessageUnsafe#read

NioServerSocketChannel#doReadMessages

ServerBootstrap#ServerBootstrapAcceptor#channelRead

5. Netty源码_UnpooledHeapByteBuf详解

本篇文章我们讲解缓存区 ByteBuf 八大主要类型中两种,未池化堆缓冲区 UnpooledHeapByteBuf 和 未池化不完全堆缓冲区 UnpooledUnsafeHeapByteBuf

UnpooledHeapByteBuf 是 java 堆缓冲区的实现,而且它推荐使用 UnpooledByteBufAllocator.heapBuffer(int, int) , Unpooled.buffer(int) 和 Unpooled.wrappedBuffer(byte[]) 方式创建 UnpooledHeapByteBuf ,而不是直接调用它的构造方法 new 出来。

有三个成员属性:

它有两个构造方法,一个是创建的时候没有内容,一个创建的时候就带有内容数据。

分配新的字节数组。

替换缓存区的字节数组 array ,必须将 tmpNioBuf 设置为 null 。

你会发现最后都是调用 HeapByteBufUtil 对应方法,这个类 HeapByteBufUtil 我们后面再说。

根据目标缓存区 dst 类型不同,使用的方式也不同。

FileChannel 就是 GatheringByteChannel 的子类。

都是调用 HeapByteBufUtil 对应方法,这个类 HeapByteBufUtil 我们后面再说。

我们知道这个方法是在 类中定义的,当引用计数变成 0 的时候,就会调用这个 deallocate() 方法,释放持有的资源。

仔细阅读 UnpooledUnsafeHeapByteBuf 源码,你会发现这个类很简单,它是 UnpooledHeapByteBuf 的子类,与 UnpooledHeapByteBuf 区别就两个方面。

UnpooledUnsafeHeapByteBuf 是通过 PlatformDependent.allocateUninitializedArray 创建数组,利用 Unsafe 来加快数据的访问

UnpooledUnsafeHeapByteBuf 是通过 UnsafeByteBufUtil 工具类获取基本数据类型的数据。

先明确一个概念,什么是大端,什么是小端。

例如获取 short

就是通过右移位运算和 或 | 位运算,实现数的拼接。

利用左移位运算,将高位数据转换成 byte 类型存储;再使用 (byte) 类型强转,只保留低八位的数据存储。

6. Netty源码_UnpooledDirectByteBuf详解

本篇文章我们讲解缓存区 ByteBuf 八大主要类型中两种,未池化直接缓冲区 UnpooledDirectByteBuf 和 未池化不安全直接缓冲区 UnpooledUnsafeDirectByteBuf 。

UnpooledDirectByteBuf 一个基于 NIO ByteBuffer 的缓冲区。
建议使用 UnpooledByteBufAllocator.directBuffer(int, int) , Unpooled.directBuffer(int) 和 Unpooled.wrappedBuffer(ByteBuffer) ;而不是显式调用构造函数。

有四个成员属性:

通过 allocateDirect(initialCapacity) 方法创建一个新的 NIO 缓存区实例来初始化此缓存区对象。

利用现有的 NIO 缓存区创建此缓存区。

通过 NIO 缓存区 buffer 对应方法获取基本数据类型数据。

根据目标缓存区 dst 类型不同,处理的方式也不同。

你会发现这些方法都是获取此缓存区对应 NIO 缓存区 ByteBuffer 对象,调用 ByteBuffer 对象的方法,与 IO 流的交互,进行数据传输

和 get 系列方法一样, set 系列的实现也是靠 NIO 缓存区 ByteBuffer 对应方法。

剩余方法也几乎都是和 NIO 缓存区 ByteBuffer 有关,而且也不难,就不做过多介绍了。

UnpooledDirectByteBuf 主要是通过 NIO 缓存区 buffer 来存储数据。而它获取和设置数据,也都是通过 NIO 缓存区对应方法实现的。

光看介绍,和 UnpooledDirectByteBuf 没有任何区别。它也是 UnpooledDirectByteBuf 的子类。

那么 UnpooledUnsafeDirectByteBuf 和 UnpooledDirectByteBuf 不同处在那里呢?

通过复习 setByteBuffer 方法,获取 NIO 缓存区 buffer 对应的直接内存地址。

通过 UnsafeByteBufUtil 对应方法,直接从内存地址获取对应基本类型数据。

通过 UnsafeByteBufUtil 对应方法,直接向内存地址设置对应基本类型数据。

只有这个类型 hasMemoryAddress() 方法才会返回 true 。

UnpooledUnsafeDirectByteBuf 就是通过直接从内存地址中获取和设置数据的方式,提高性能。

7. [Netty源码分析]ByteBuf(一)

ByteBuf通过两个指针协助读写操作,读操作使用readerIndex,写操作使用writerIndex.

readerIndex、writerIndex初始值是0,写入数据时writerIndex增加,读取数据时readerIndex增加,但是readerIndex不会超过writerIndex.

读取之后,0-readerIndex之间的空间视为discard的,调用discardReadByte方法可以释放这一部分空间,作用类似于ByteBuffer的compact方法.readerIndex-writerIndex之间的数据是可读的,等价于ByteBuffer中position-limit之间的数据.

writerIndex-capacity之间的空间是可写的,等价于ByteBuffer中limit-capacity之间的空间.

读只影响readerIndex、写只影响writerIndex,读写之间不需要调整指针位置,所以相较于NIO的ByteBuffer,可以极大的简化读写操作

调用discardReadBytes会发生字节数组的内存复制,所以频繁调用会导致性能下降

ByteBuf对write操作进行了封装,有ByteBuf的write操作负责进行剩余咳哟好难过空间的校验,如果可用缓冲区不足,ByteBuf会自动进行动态扩展。对于使用者而言不需要关心底层的校验和扩展细节,只需要不超过capacity即可

对缓冲区进行读操作时,有的时候我们需要对之前的操作进行回滚,读操作并不会改变缓冲区的内容,回滚主要是重新设置索引信息

Mark:将当前的位置指针被分到mark变量中

Reset:恢复位置指针为mark中的变量值

ByteBuf有readerIndex、writerIndex,所以有四个相应的方法

markReaderIndex: 将当前readerIndex备份到markedReaderIndex中

resetReaderIndex: 将当前readerIndex设置为markedReaderIndex

markWriterIndex: 将当前readerIndex备份到markedWriterIndex中

resetWriterIndex: 将当前readerIndex设置为markedWriterIndex

3)slice:
返回当前ByteBuf的可读子缓冲区,即从readerIndex到writerIndex的ByteBuf,返回的ByteBuf和原有缓冲区共享内容,但是维护独立的索引.当修改其中一个ByteBuf的内容时,另一个也会改变,即双方持有的是同一个对象的引用

常见类:

相比于PooledHeapByteBuf,UnpooledHeapByteBuf的实现更加简单,也不容易出现内存管理的问题,所以才性能满足的情况下,推荐使用UnpooledHeapByteBuf

在I/O通信线程的读写缓冲区中使用DirectByteBuf,后端业务消息的编码使用HeapByteBuf,这样的组合性能最优

8. Netty 源码解析 ——— ChannelConfig 和 Attribute

嗯,本文与其说是ChannelConfig、Attribute源码解析,不如说是对ChannelConfig以及Attribute结构层次的分析。因为这才是它们在Netty中使用到的重要之处。

在 Netty 源码解析 ——— 服务端启动流程 (下) 中说过,当我们在构建NioServerSocketChannel的时候同时会构建一个NioServerSocketChannelConfig对象赋值给NioServerSocketChannel的成员变量config。

而这一个NioServerSocketChannelConfig是当前NioServerSocketChannel配置属性的集合。NioServerSocketChannelConfig主要用于对NioServerSocketChannel相关配置的设置(如,网络的相关参数配置),比如,配置Channel是否为非阻塞、配置连接超时时间等等。

NioServerSocketChannelConfig其实是一个ChannelConfig实例。ChannelConfig表示为一个Channel相关的配置属性的集合。所以NioServerSocketChannelConfig就是针对于NioServerSocketChannel的配置属性的集合。

ChannelConfig是Channel所需的公共配置属性的集合,如,setAllocator(设置用于channel分配buffer的分配器)。而不同类型的网络传输对应的Channel有它们自己特有的配置,因此可以通过扩展ChannelConfig来补充特有的配置,如,ServerSocketChannelConfig是针对基于TCP连接的服务端ServerSocketChannel相关配置属性的集合,它补充了针对TCP服务端所需的特有配置的设置setBacklog、setReuseAddress、setReceiveBufferSize。

DefaultChannelConfig作为ChannelConfig的默认实现,对ChannelConfig中的配置提供了默认值。

接下来,我们来看一个设置ChannelConfig的流程:
serverBootstrap.option(ChannelOption.SO_REUSEADDR, true);
我们可以在启动服务端前通过ServerBootstrap来进行相关配置的设置,该选项配置会在Channel初始化时被获取并设置到Channel中,最终会调用底层ServerSocket.setReuseAddress方法来完成配置的设置。
ServerBootstrap的init()方法:

首先对option和value进行校验,其实就是进行非空校验。
然后判断对应的是哪个常量属性,并进行相应属性的设置。如果传进来的ChannelOption不是已经设定好的常量属性,则会打印一条警告级别的日志,告知这是未知的channel option。
Netty提供ChannelOption的一个主要的功能就是让特定的变量的值给类型化。因为从’ChannelOption<T> option’和’T value’可以看出,我们属性的值类型T,是取决于ChannelOption的泛型的,也就属性值类型是由属性来决定的。

这里,我们可以看到有个ChannelOption类,它允许以类型安全的方式去配置一个ChannelConfig。支持哪一种ChannelOption取决于ChannelConfig的实际的实现并且也可能取决于它所属的传输层的本质。

可见ChannelOption是一个Consant扩展类,Consant是Netty提供的一个单例类,它能安全去通过’==’来进行比较操作。通过ConstantPool进行管理和创建。
常量由一个id和name组成。id:表示分配给常量的唯一数字;name:表示常量的名字。

如上所说,Constant是由ConstantPool来进行管理和创建的,那么ConstantPool又是个什么样的类了?

首先从constants中get这个name对应的常量,如果不存在则调用newConstant()来构建这个常量tempConstant,然后在调用constants.putIfAbsent方法来实现“如果该name没有存在对应的常量,则插入,否则返回该name所对应的常量。(这整个的过程都是原子性的)”,因此我们是根据putIfAbsent方法的返回来判断该name对应的常量是否已经存在于constants中的。如果返回为null,则说明当前创建的tempConstant就为name所对应的常量;否则,将putIfAbsent返回的name已经对应的常量值返回。(注意,因为ConcurrentHashMap不会允许value为null的情况,所以我们可以根据putIfAbsent返回为null则代表该name在此之前并未有对应的常量值)

正如我们前面所说的,这个ConstantPool<ChannelOption<Object>> pool(即,ChannelOption常量池)是ChannelOption的一个私有静态成员属性,用于管理和创建ChannelOption。

这些定义好的ChannelOption常量都已经存储数到ChannelOption的常量池(ConstantPool)中了。

注意,ChannelOption本身并不维护选项值的信息,它只是维护选项名字本身。比如,“public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");”👈这只是维护了“SO_RCVBUF”这个选项名字的信息,同时泛型表示选择值类型,即“SO_RCVBUF”选项值为Integer。

好了,到目前为止,我们对Netty的ChannelOption的设置以及底层的实现已经分析完了,简单的来说:Netty在初始化Channel时会构建一个ChannelConfig对象,而ChannelConfig是Channel配置属性的集合。比如,Netty在初始化NioServerSocketChannel的时候同时会构建一个NioServerSocketChannelConfig对象,并将其赋值给NioServerSocketChannel的成员变量config,而这个config(NioServerSocketChannelConfig)维护了NioServerSocketChannel的所有配置属性。比如,NioServerSocketChannelConfig提供了setConnectTimeoutMillis方法来设置NioServerSocketChannel连接超时的时间。
同时,程序可以通过ServerBootstrap或Boostrap的option(ChannelOption<T> option, T value)方法来实现配置的设置。这里,我们通过ChannelOption来实现配置的设置,ChannelOption中已经将常用的配置项预定义为了常量供我们直接使用,同时ChannelOption的一个主要的功能就是让特定的变量的值给类型化。因为从’ChannelOption<T> option’和’T value’可以看出,我们属性的值类型T,是取决于ChannelOption的泛型的,也就属性值类型是由属性来决定的。

一个attribute允许存储一个值的引用。它可以被自动的更新并且是线程安全的。
其实Attribute就是一个属性对象,这个属性的名称为AttributeKey<T> key,而属性的值为T value。

我们可以通过程序ServerBootstrap或Boostrap的attr方法来设置一个Channel的属性,如:
serverBootstrap.attr(AttributeKey.valueOf("userID"), UUID.randomUUID().toString());
当Netty底层初始化Channel的时候,就会将我们设置的attribute给设置到Channel中:

如上面所说,Attribute就是一个属性对象,这个属性的名称为AttributeKey<T> key,而属性的值为T value。
而AttributeKey也是Constant的一个扩展,因此也有一个ConstantPool来管理和创建,这和ChannelOption是类似的。

Channel类本身继承了AttributeMap类,而AttributeMap它持有多个Attribute,这些Attribute可以通过AttributeKey来访问的。所以,才可以通过channel.attr(key).set(value)的方式将属性设置到channel中了(即,这里的attr方法实际上是AttributeMap接口中的方法)。

AttributeKey、Attribute、AttributeMap间的关系:
AttributeMap相对于一个map,AttributeKey相当于map的key,Attribute是一个持有key(AttributeKey)和value的对象。因此在map中我们可以通过AttributeKey key获取Attribute,从而获取Attribute中的value(即,属性值)。

Q:ChannelHandlerContext和Channel都提供了attr方法,那么它们设置的属性作用域有什么不同了?
A:在Netty 4.1版本之前,它们两设置的属性作用域确实存在着不同,但从Netty 4.1版本开始,它们两设置的属性的作用域已经完全相同了。

若文章有任何错误,望大家不吝指教:)

圣思园《精通并发与Netty》

9. 如何构建一个基于netty的后端服务器

Netty服务端创建
当我们直接使用JDK NIO的类库开发基于NIO的异步服务端时,需要使用到多路复用器Selector、ServerSocketChannel、SocketChannel、ByteBuffer、SelectionKey等等,相比于传统的BIO开发,NIO的开发要复杂很多,开发出稳定、高性能的异步通信框架,一直是个难题。
Netty为了向使用者屏蔽NIO通信的底层细节,在和用户交互的边界做了封装,目的就是为了减少用户开发工作量,降低开发难度。ServerBootstrap是Socket服务端的启动辅助类,用户通过ServerBootstrap可以方便的创建Netty的服务端。

10. Netty源码-内存泄漏检测toLeakAwareBuffer

Netty在实现 ByteBuf 时采用了引用计数法进行 ByteBuf 的回收,使用引用计数法进行回收的 ByteBuf 都扩展了 类,在使用 时需要调用 .retain 方法递增引用计数器,在使用完毕时则需要调用 .release 方法递减引用计数器,当计数器为 0 时,会进行 ByteBuf 的回收工作:池化的 ByteBuf 不会进行实际的内存释放,会将占用的内存归还给内存池,非池化的 ByteBuf 则会直接释放内存(为了叙述简单,后面释放内存则指真正释放内存或者将内存归还给内存池)。

通过上面的描述可知, ByteBuf 的正确回收依赖 retain 和 release 方法的正确调用,内存提前释放(即在使用 ByteBuf 时没有调用 retain 方法,导致提前释放)应用会报错,用户也能及时感知到;但是如果使用完 ByteBuf 忘了调用 release 则会导致内存不能及时得到回收,造成内存泄漏,且内存泄漏用户无法及时感知,久而久之就会发生OOM。为了解决这种问题,Netty采用了内存泄漏检测机制,发生内存泄漏时会通过日志将内存泄漏信息打印出来,报告给用户。

Netty的内存泄漏检测使用了 WeakReference ,即弱引用,了解过Java四种引用类型(强、软、弱、虚)和引用队列( ReferenceQueue )的读者知道,弱引用持有的对象会在虚拟机触发GC时(不管回收之后内存是否够用)被回收掉,如果使用具有引用队列参数的构造函数实例化 WeakReference 时,弱引用持有的对象在GC被回收时,弱引用自身会被放入引用队列。

为了后面能更好的理解Netty内存泄漏检测的细节,下面先看几个弱引用的例子,在下面的几个例子中,我们使用的数据类和自定义的弱引用类子类如下:

好了,三个例子已经介绍完毕,后面在介绍Netty内存泄漏检测时就使用了这里的例子结果,在具体介绍时会和这里的例子一一对应。

Netty中将普通 ByteBuf 转为具有内存泄漏检测功能的 ByteBuf 是通过 AbstractByteBufAllocator.toLeakAwareBuffer 方法实现的,我们直接在Eclipse中看该方法的调用层次即可知道Netty在哪里对 ByteBuf 进行了转换,该方法调用如下图所示:

可见池化内存分配器在分配heap或者direct ByteBuf 时都进行了转换,非池化内存分配器仅在分配direct ByteBuf 时进行了转换。个人理解时采用池化内存需要特别关注内存释放,否则为了实现池化内存预先分配的一大块内存会因为没有释放被很快分配完,造成后面没有内存进行分配。非池化分配的直接内存也需要特别注意释放,放置内存泄漏;非池化分配的heap内存(其实就是一个 byte 数组)则可以在对象被回收时同时被回收掉,发生内存泄漏的可能性较小。

本节介绍Netty中内存泄漏检测相关的类,仅做一个大致介绍,类中的重要方法我们放在后面介绍。

主要负责使用 track 方法对指定的 ByteBuf 进行内存检测泄漏进行追踪,并返回负责追踪的 ResourceLeakTracker 类实例,同时在调用 track 方法时,也会根据指定的检测级别汇报最近的内存泄漏检测结果。该类由工厂类 ResourceLeakDetectorFactory 负责实例化,默认的实现为 ResourceLeakDetector ,在 ResourceLeakDetectorFactory 类的默认实现 中,也会根据用户是否配置了 io.netty.customResourceLeakDetector 来决定采用默认实现 ResourceLeakDetector 还是使用用户自定义的 ResourceLeakDetector ,用户自定义的 ResourceLeakDetector 必须是其子类。

默认实现为 DefaultResourceLeak , DefaultResourceLeak 实现了 ResourceLeakTracker 和 ResourceLeak 接口,同时也继承了类 WeakReference ,是一个弱引用实现。首先,同上面 例2 的结果一样,如果在使用 ByteBuf 时忘了调用 .release 方法,那么将不会调用 DefaultResourceLeak.clear 方法去手动清空该弱引用持有的实际对象,在发生GC时,会由垃圾收集器对弱引用持有的实际对象进行回收,即发生了内存泄漏,同时该弱引用自身也会被加入到引用队列中,该引用队列是 ResourceLeakDetector 的成员域,上面介绍 ResourceLeakDetector 类时说到该类会在用户 track 指定 ByteBuf 是汇报检测结果,该类的汇报数据来源就是引用队列。 DefaultResourceLeak 同时还提供了 record 方法可以让用户在指定时机选择调用,这个方法可以记录用户的调用轨迹(堆栈)。 Record 同时也是一种单链表,在 DefaultResourceLeak 中就使用单链表记录用户的调用轨迹。

DefaultResourceLeak 供用户记录程序调用轨迹的类,也就是 DefaultResourceLeak.record 方法返回的对象,继承自 Throwable ,因此可以使用 Throwable.getStackTrace 方法获得调用轨迹信息,打印在内存泄漏报告中可以让用户更好的排除内存泄漏问题。

在上面介绍 ResourceLeakTracker 时,说到其默认实现为 DefaultResourceLeak , DefaultResourceLeak 提供了 record 方法记录用户的调用轨迹,用户可在调用 ByteBuf 方法时调用 record 方法记录调用轨迹,调用的频率越多,后面在汇报内存泄漏情况时就能打印出越详细的信息,这样也能更方便的排查问题。

Netty提供了两个 ByteBuf 的封装类供选择,就对应不同的 record 调用频率,每个封装类都持有 ResourceLeakTracker 对象,Netty根据配置的内存检测级别(下一节介绍相关配置参数)使用不同的 ByteBuf 封装类。

Netty提供的两个 ByteBuf 封装类就是 和 , 是 的子类, 类仅仅持有 ResourceLeakTracker 对象,但是看其源码,发现没有调用过 record 方法,所以只能知道是否发生了内存泄漏时,无法打印出任何调用轨迹信息。 作为 的子类,在 ByteBuf 的多个方法中调用了 record 方法,所以在发生内存泄漏时,能够打印出比较详细的调用轨迹信息。

在 类中使用了配置参数 io.netty.leakDetection.acquireAndReleaseOnly 来控制是否只是在调用增加或减少引用计数器的方法时才调用 record 方法记录调用轨迹,默认为false。 中 retain 和 release 方法因为改变了引用计数器就直接调用了 record 方法,而该类中的其他方法则根据 io.netty.leakDetection.acquireAndReleaseOnly 的配置决定是否调用 record 方法,这里为了节省篇幅就不列出 类中调用 record 的方法了,读者可自行查看。

在介绍相关配置参数之前,我们先看下Netty提供的内存泄漏检测级别:

Level.ADVANCED 和 Level.PARANOID 使用的 ByteBuf 包装类都是 ,我们上面介绍 ResourceLeakDetector 类时提到该类使用 track 方法对指定的 ByteBuf 进行内存检测泄漏进行追踪,并返回负责追踪的 ResourceLeakTracker 类实例,同时在调用 track 方法时,也会根据指定的检测级别汇报最近的内存泄漏检测结果。如果内存泄漏检测级别为 Level.PARANOID 时则每次调用 track 方法都会进行内存泄漏报告;如果级别为 Level.ADVANCED 或者 Level.SIMPLE 则会以一定频率进行内存泄漏报告,而不是每次 track 都进行报告。

是否关闭Netty内存泄漏检测功能,默认为false。如果该参数配置为false,则默认的内存泄漏检测级别根据此参数的配置为 Level.DISABLED ,否则默认的级别为 Level.SIMPLE 。

配置内存泄漏检测级别的参数,用于老版本的配置参数。

新的内存泄漏检测级别参数,如果没有配置,则会采用老版本参数配置的级别作为最终配置。

在第4节介绍内存泄漏检测相关类时,我们介绍过 DefaultResourceLeak 提供了 record 方法记录用户的调用轨迹,如果当前保存的调用轨迹记录数 Record 大于参数 io.netty.leakDetection.targetRecords 配置的值,那么会以一定的概率(1/2^n)删除头结点之后再加入新的记录,当然也有可能不删除头结点直接新增新的记录。

该参数的默认为4。

上面介绍过,在 类中使用了配置参数 io.netty.leakDetection.acquireAndReleaseOnly 来控制是否只是在调用增加或减少引用计数器的方法时才调用 record 方法记录调用轨迹,默认为false。

在介绍 ResourceLeakDetector 类时提到过,默认的 ResourceLeakDetector 类就是 ResourceLeakDetector ,但是用户可以使用参数 io.netty.customResourceLeakDetector 来决定采用默认实现 ResourceLeakDetector 还是使用用户自定义的 ResourceLeakDetector 。

我们在第二节介绍了Netty中将普通 ByteBuf 转为具有内存泄漏检测功能的 ByteBuf 是通过 AbstractByteBufAllocator.toLeakAwareBuffer 方法实现的。

这里我们先看下该方法的源码:

上面的源码中是调用 AbstractByteBuf.leakDetector.track(buf) 返回 ResourceLeakTracker 类对象的,这里我们看下默认的 ResourceLeakDetector 中 track 方法实现:

我们看到 AbstractByteBufAllocator.toLeakAwareBuffer 对 ResourceLeakDetector.track 返回的 DefaultResourceLeak 和传入的 ByteBuf 对象进行封装,返回了具有内存泄漏检测功能的 ByteBuf 封装类 或其子类 。如果应用程序在使用 ByteBuf 正确调用了 retain 和 release 方法,则在引用计数器为0时,则会清除弱引用持有的实际对象,发生GC时, DefaultResourceLeak 也不会被放入引用队列中(见前面第2节 例3 结果)。

但是如果应用程序在使用 ByteBuf 没有正确调用 retain 和 release 方法,则不会清除弱引用持有的实际对象,此时如果实际上已经没有强引用指向该 ByteBuf ,那么在发生GC时,垃圾收集器会回收该 ByteBuf ,而弱引用 DefaultResourceLeak 会被放入引用队列中(见前面第2节 例2 结果),加入到引用队列中的就是识别到的发生内存泄漏的 ByteBuf 。在 ResourceLeakDetector.track 方法中调用的 reportLeak 输出的就是引用队列中的弱引用 DefaultResourceLeak :

到这里,已经基本上介绍完Netty内存检测的实现原理,下面我们再看下 DefaultResourceLeak.record 是如何记录调用轨迹的:

最后我们再看下 Record 是如何输出调用轨迹的,前面我们说到 Record 继承自类 Throwable ,因此可使用 getStackTrace 方法获取实例化该对象时的调用轨迹,所以上面在输出内存泄漏报告时就调用了 Record.toString 方法:

热点内容
真我手机如何解除手机密码 发布:2024-05-04 18:24:44 浏览:707
数据库嵌套 发布:2024-05-04 18:24:29 浏览:145
豌豆荚源码 发布:2024-05-04 18:10:54 浏览:116
苹果消息的声音安卓怎么弄 发布:2024-05-04 18:06:23 浏览:554
减配配置有哪些 发布:2024-05-04 18:04:58 浏览:962
查询密码单是什么 发布:2024-05-04 17:54:03 浏览:40
安卓系统不支持网络怎么办 发布:2024-05-04 17:49:31 浏览:128
oraclesqlserver 发布:2024-05-04 17:49:16 浏览:47
关爱脚本 发布:2024-05-04 17:43:47 浏览:422
linuxshellif 发布:2024-05-04 17:09:47 浏览:17