「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。
不论我们在前面学习NIO的ByteBuffer,还是现在Netty当中的ByteBuf,其都有使用直接内存的方式。
在Netty当中,我们使用完直接内存,需要去手动进行释放,而不应该等待GC去进行回收,以减少发生内存溢出的风险。
一、ByteBuf的种类
关于其种类,有很多种,我们根据前面提到的池化机制,将其主要分为两大类,每一类当当中又分为堆内存和直接内存:
- UnpooledHeapByteBuf:非池化堆内存ByteBuf,受JVM内存管理,可以等待GC回收。
- UnpooledDirectByteBuf:非池化直接内存ByteBuf,不收JVM管理,虽然可以受GC回收,但不是及时的,可能会发生内存溢出,需要手动进行回收。
- PooledByteBuf:池化ByteBuf,这种有更复杂的回收范式,后面通过源码分析,具体查看其实现细节。
- PooledHeapByteBuf:池化堆内存ByteBuf
- PooledDirectByteBuf:池化直接内存ByteBuf
二、直接内存回收原理
在前面的文章中,我们简单聊到过ByteBuf的结构:
public abstract class ByteBuf implements ReferenceCounted
如上所示,其实现了ReferenceCounted的接口,接口翻译过来叫做“引用计数”。
相信学过jvm GC的同学应该有所了解“引用计数法”,当一个对象有引用时,我们就对计数器加1,反之就减1,但是引用计数法无法处理环形垃圾,所以后面提出了“根可达算法”,简单提一下,需要了解细节的朋友可以看我的专题【JVM】。
此处的引用计数,用于ByteBuf的直接内存回收,我们看下其主要的方法:
1 | csharp复制代码public interface ReferenceCounted { |
所有的ByteBuf都会实现这个接口,当一个新的ReferenceCounted被实例化时,它以1的引用计数开始。 retain()增加引用计数,而release()减少引用计数。 如果引用计数减少到0 ,对象将被释放,并且访问释放的对象通常会导致访问冲突。
通过下面的代码简单试用一下:
1 | csharp复制代码 public static void main(String[] args) { |
结果:
1 | yaml复制代码初始化后的引用计数1 |
当引用计数变为0后,整个内存就释放了,再次使用会抛出异常,重新尝试增加引用计数也会跑出异常,只能进行重新分配。
三、内存释放使用方式
3.1 手动释放
前面简单了解了关于内存释放的内容,那么我们应该如何使用呢?是不是可以向我们习惯的java代码一样,在finally当中调用呢?
1 | csharp复制代码try { |
直接给出结论,是不行的。
前面我们介绍时候就说过,会有几率造成内存溢出的,即使不会发生也会造成内存的浪费。
前面的文章当中,我们学习了Pipeline和Handler。通常我们会将一个byteBuf传递给另一个channelHandler去处理,是存在一个传递性的。这里面存在两种情况:
- 假设一共有5个channelHandler,在第二个当中,将byteBuf转换成了java对象,然后将对象传递给第三个channelHandler,此时byteBuf就没有用了,所以此时就应该释放。
- 一直以byteBuf传递,直到最后一个channelHandler才进行释放。
总结一句话:最后谁用完了,谁就负责释放。
建议:如果确定这个buf在最后时刻用完了,而又无法确定当前有多少个引用计数,使用如下两种方式释放:
- 循环调用release(),知道返回true。
- 通过refCnt()获取当前的引用计数,然后调用release(int refCnt)释放。
3.2 tail和head自动释放
还记得前面将Pipeline和Handler时,提到了关于head和tail的概念,除了我们自己添加的Handler以外,会默认有一个头和尾的处理器。
在这两个处理器当中,也会有自动回收内存的保底能力,但是前提是要求我们将byteBuf传递到head或tail当中才行,对于中途就转换类型的,仍然需要我们自己去释放资源。
前面我们还学习过入站处理器和出栈处理器,其中入站处理器传递内容需要使用channelRead()方法,而在出站处理器传递参数需要使用write方法,这将作为我们跟踪代码的标记。
下面我们简单跟踪下源码,看看是如何实现的内存释放。
我们跟踪pipeline的addLast方法,跟踪到了AbstractChannelHandlerContext这个抽象类,其有两个实现类:
刚好对应我们的head和tail处理器。
3.2.1 TailContext
首先看tail处理器,实现了ChannelInboundHandler,即入站处理器,进行入站首尾工作。
1 | scala复制代码final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler |
找到channelRead方法:
1 | typescript复制代码 public void channelRead(ChannelHandlerContext ctx, Object msg) { |
继续跟踪onUnhandledInboundMessage
1 | typescript复制代码 protected void onUnhandledInboundMessage(Object msg) { |
发现其中的引用计数工具类,调用了release方法:
ReferenceCountUtil.release(msg);
判断msg是否是实现了ReferenceCounted ?是就进行是否,否则返回false。
1 | typescript复制代码 public static boolean release(Object msg) { |
3.2.1 HeadContext
查看HeadContext,实现了ChannelOutboundHandler,即出站处理器,进行出站首尾工作。
1 | scala复制代码final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler |
找到其write方法:
1 | arduino复制代码 public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { |
继续跟踪write:
1 | arduino复制代码 public final void write(Object msg, ChannelPromise promise) { |
在上面的代码中,仍然发现了
ReferenceCountUtil.release(msg)
其他代码此文暂时不做讲解了。
无论是head,还是tail,都需要将buf传递过来,才能进行释放。
本文暂时介绍这些,后面继续,有帮助的话点个赞吧。
本文转载自: 掘金