Netty 内存管理
Netty的高效和易用也得益于他强大的内存管理,下面这个章节就让我们一起来研究Netty是如何进行内存管理的
Netty ByteBuf缓冲区
官方列出了一些ByteBuf的特性:
- 需要的话,可以自定义buffer类型;
- 通过组合buffer类型,可实现透明的zero-copy;
- 提供动态的buffer类型,如StringBuffer一样,容量是按需扩展;
- 无需调用flip()方法;
- 常常often比ByteBuffer
网络数据的基本单位总是字节。Java NIO 提供了ByteBuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐
Netty 的ByteBuffer 替代品是ByteBuf,一个强大的实现,既解决了JDK API 的局限性,又为网络应用程序的开发者提供了更好的API
ByteBuf结构
ByteBuf 维护了读写两个不同的索引,名称以read 或者write 开头的ByteBuf 方法,将会推进其对应的索引,而名称以set 或者get 开头的操作则不会
ByteBuf 的使用
- 堆缓冲区(heapBuffer)
最常用的ByteBuf 模式是将数据存储在JVM 的堆空间中。这种模式被称为支撑数组(backing array),它能在没有使用池化的情况下提供快速的分配和释放。可以由hasArray()来判断检查ByteBuf 是否由数组支撑。如果不是,则这是一个直接缓冲区 - 直接缓冲区(directBuffer)
直接缓冲区是另外一种ByteBuf 模式。
直接缓冲区的主要缺点是,相对于基于堆的缓冲区,它们的分配和释放都较为昂贵。 - 复合缓冲区
随机访问索引/顺序访问索引/读写操作
和普通的Java字节数组中一样,ByteBuf
的索引是从零开始的:第一个字节的索引是0,最后一个字节的索引总是capacity() - 1
。使用那些需要一个索引值参数( 随机访问,也即是数组下标)的方法(的其中)之一来访问数据既不会改变readerIndex 也不会改变writerIndex。如果有需要,也可以通过调用readerIndex(index)或者writerIndex(index)来手动移动这两者。 顺序访问通过索引访问
有两种类别的读、写操作:
- get()和set()操作,从给定的索引开始,并且保持索引不变;get+数据类型(bool.byte,int,short,long,bytes),将自动读取类型长度的数据
- read()和write()操作,从给定的索引开始,并且会根据已经访问过的字节数对索引进行调整
更多的操作
函数名称 | 描述 |
---|---|
isReadable() | 如果至少有一个字节可供读取,则返回true |
isWritable() | 如果至少有一个字节可被写入,则返回true |
readableBytes() | 返回可被读取的字节数 |
writableBytes() | 返回可被写入的字节数 |
capacity() | 返回ByteBuf 可容纳的字节数。在此之后,它会尝试再次扩展直到达到maxCapacity() |
maxCapacity() | 返回ByteBuf 可以容纳的最大字节数 |
hasArray() | 如果ByteBuf 由一个字节数组支撑,则返回true |
array() | 如果 ByteBuf 由一个字节数组支撑则返回该数组;否则,它将抛出一个UnsupportedOperationException 异常 |
可丢弃字节
为可丢弃字节的分段包含了已经被读过的字节。通过调用discardReadBytes()
方法,可以丢弃它们并回收空间。这个分段的初始大小为0,存储在readerIndex 中,会随着read 操作的执行而增加(get操作不会移动readerIndex)
缓冲区上调用discardReadBytes()方法后,可丢弃字节分段中的空间已经变为可写的了。频繁地调用discardReadBytes()方法以确保可写分段的最大化,但是请注意,这将极有可能会导致内存复制,因为可读字节必须被移动到缓冲区的开始位置。建议只在有真正需要的时候才这样做,例如,当内存非常宝贵的时候
可读字节
ByteBuf
的可读字节分段存储了实际数据。新分配的、包装的或者复制的缓冲区的默认的readerIndex
值为0
可写字节
可写字节分段是指一个拥有未定义内容的、写入就绪的内存区域。新分配的缓冲区的writerIndex 的默认值为0。任何名称以write 开头的操作都将从当前的writerIndex 处开始写数据,并将它增加已经写入的字节数
索引管理
调用markReaderIndex()
、markWriterIndex()
、resetWriterIndex()
和resetReaderIndex()
来标记和重置ByteBuf的readerIndex和writerIndex
也可以通过调用readerIndex(int)
或者writerIndex(int)
来将索引移动到指定位置。试图将任何一个索引设置到一个无效的位置都将导致一个IndexOutOfBoundsException
可以通过调用clear()
方法来将readerIndex和writerIndex 都设置为0
。注意,这并不会清除内存中的内容
查找操作
在ByteBuf中有多种可以用来确定指定值的索引的方法。最简单的是使用indexOf()
方法
较复杂的查找可以通过调用forEachByte()
,代码展示了一个查找回车符(\r)的例子
ByteBuf buf = Unpooled.copiedBuffer(str.getBytes());
int index = buf.forEachByte(ByteProcessor.FIND_CR);
派生缓冲区
派生缓冲区为ByteBuf提供了以专门的方式来呈现其内容的视图。这类视图是通过以下方法被创建的:
- duplicate()
- slice()
slice(int, int) - Unpooled.unmodifiableBuffer(…)
- order(ByteOrder)
- readSlice(int)
每个这些方法都将返回一个新的ByteBuf实例,它具有自己的读索引、写索引和标记索引。其内部存储和Netty的ByteBuf一样也是共享的
ByteBuf复制:如果需要一个现有缓冲区的真实副本,请使用copy()
或者copy(int, int)
方法。不同于派生缓冲区,由这个调用所返回的ByteBuf拥有独立的数据副本
引用计数
引用计数是一种通过在某个对象所持有的资源不再被其他对象引用时释放该对象所持有的资源来优化内存使用和性能的技术。Netty在第4版中为ByteBuf引入了引用计数技术interface ReferenceCounted
工具类
ByteBufUtil提供了用于操作ByteBuf的静态的辅助方法。因为这个API 是通用的,并且和池化无关,所以这些方法已然在分配类的外部实现
这些静态方法中最有价值的可能就是hexdump()
方法,它以十六进制的表示形式打印ByteBuf的内容。这在各种情况下都很有用,例如,出于调试的目的记录ByteBuf的内容。十六进制的表示通常会提供一个比字节值的直接表示形式更加有用的日志条目,此外,十六进制的版本还可以很容易地转换回实际的字节表示
另一个有用的方法是boolean equals(ByteBuf, ByteBuf)
,它被用来判断两个ByteBuf实例的相等性
资源释放
当某个ChannelInboundHandler的实现重写channelRead()
方法时,它要负责显式地释放与池化的ByteBuf实例相关的内存。Netty为此提供了一个实用方法ReferenceCountUtil.release()
Netty将使用WARN级别的日志消息记录未释放的资源,使得可以非常简单地在代码中发现违规的实例。但是以这种方式管理资源可能很繁琐。一个更加简单的方式是使用SimpleChannelInboundHandler,SimpleChannelInboundHandler会自动释放资源
- 入站请求:Netty的EventLoop在处理Channel的读操作时进行分配ByteBuf,对于这类ByteBuf,需要我们自行进行释放,有三种方式,或者使用
SimpleChannelInboundHandler
,或者在重写channelRead()
方法使用ReferenceCountUtil.release()
或者使用ctx.fireChannelRead(buf)
继续向后传递 - 出站请求:不管ByteBuf是否由我们的业务创建的,当调用了
write
或者writeAndFlush
方法后,Netty会自动替我们释放,不需要我们业务代码自行释放
ByteBufAllocator 内存分配接口
Netty 通过interface ByteBufAllocator分配我们所描述过的任意类型的ByteBuf 实例,下面是常用接口函数:
函数名称 | 描述 |
---|---|
buffer() | 返回一个基于堆或者直接内存存储的ByteBuf |
heapBuffer() | 返回一个基于堆内存存储的ByteBuf |
directBuffer() | 返回一个基于直接内存存储的ByteBuf |
compositeBuffer() | 返回一个可以通过添加最大到指定数目的基于堆的或者直接内存存储的缓冲区来扩展的CompositeByteBuf |
Netty提供了两种ByteBufAllocator的实现
- PooledByteBufAllocator:实现池化了ByteBuf的实例以提高性能并最大限度地减少内存碎片
- UnpooledByteBufAllocator:实现不池化ByteBuf实例,并且在每次它被调用时都会返回一个新的实例
Netty4.1默认使用了PooledByteBufAllocator。
Unpooled 非池化缓冲区
Netty 提供了一个简单的称为Unpooled 的工具类,它提供了静态的辅助方法来创建未池化的ByteBuf实例
分配过程
- 参数校验
- 判断unsafe走不同的分配方法
- new byte[初始容量]
- set readIndex=0 writeIndex=0
Unpooled提供以下接口:
函数名称 | 描述 |
---|---|
buffer() | 返回一个未池化的基于堆内存存储的ByteBuf |
directBuffer() | 返回一个未池化的基于直接内存存储的ByteBuf |
wrappedBuffer() | 返回一个包装了给定数据的ByteBuf |
copiedBuffer() | 返回一个复制了给定数据的ByteBuf |
Pooled 池化缓冲区
4.x开发了Pooled Buffer,实现了一个高性能的buffer池,分配策略有点想Linux下的内存管理算法,伙伴算法
由于这部分内容未做深入研究,可以参考下面连接
Netty内存池化