Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)


         上文描述过,ByteBuf根据内存分可以分为堆内存和堆外内存实现,此处以非池化实现介绍,两个重要子类是UnpooledHeapByteBuf和UnpooledDirectByteBuf,因为堆外内存和堆内存除了内存分配之外,其他实现十分相似,先简单描述下堆内存实现,然后重点分析堆外内存的分配及回收策略


堆内存实现


      ByteBuf的堆内存主要由子类UnpooledHeapByteBuf实现,UnpooledHeapByteBuf是基于堆内存进行内存分配的字节缓冲区,它没有基于对象池技术实现,每次I/O的读写都会创建一个新的UnpooledHeapByteBuf。


      基础属性如下:
在这里插入图片描述
      聚合了一个ByteBufAllocator用于UnpooledHeapByteBuf的内存分配,一个byte数组作为缓冲区,最后定义了一个ByteBuffer类型的tmpNioBuf变量用于实现Netty ByteBuf到JDK NIO ByteBUffer的转换。


      在父接口基础上实现了扩容接口:

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第1张图片
      首先对新容量进行合法性校验,如果大于容量上限或者小于0,则抛出异常。

      判断新的容量是否大于当前的缓冲区容量,大于则需要进行动态扩展。创建新的缓冲区字节数组,然后System.arraycopy进行内存复制,setArray替换旧的字节数组,释放旧内存。

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第2张图片

      将ByteBuf转换为ByteBuffer,使用了NIO的ByteBuffer提供的wrap方法,将byte数组转换为ByteBuffer对象。



堆外内存及回收策略


      入口在UnpooledByteBufAllocator#newDirectBuffer方法。Netty提供了两种内存策略,一种是使用java.nio.DirectByteBuffer实现,另一种是noCleaner策略实现。用不同的对象实现,均继承自UnpooledUnsafeDirectByteBuf。

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第3张图片

DirectByteBuffer实现


      实现类为InstrumentedUnpooledUnsafeDirectByteBuf,继承关系如下图:

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第4张图片
重写了内存分配和回收的方法:

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第5张图片

分配内存时,直接调用java.nio.DirectByteBuffer实现。


在这里插入图片描述

DIrectByteBuffer的内存分配代码如下:

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第6张图片
      向Bits类申请内存,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限,堆外内存的限额默认与堆内内存(由-XMX 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。

跟踪reserveMemory:

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第7张图片
      如果已经超限,会主动执行Sytem.gc(),试图回收DirectByteBuffer,并释放部分堆外内存。然后休眠一百毫秒,如果内存还是不足,就抛出OOM异常。

      如果内存足够,则调用Unsafe分配堆外内存返回内存基地址。

      最后,创建一个Cleaner,并把代表清理动作的Deallocator类绑定,降低Bits里的totalCapacity,并调用Unsafe调free去释放内存。

      Cleaner类继承了PhantomReference虚引用类,是一个虚引用对象(不影响对象的生命周期),GC时发现它除了PhantomReference外已不可达(持有它的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。而PhantomReference继承自Reference。Reference在静态块中启动了一个handler守护线程,死循环处理tryHandlerPending方法,主要逻辑是判断不可达对象是否为Cleaner,如果是,则调用其clean方法回收堆外内存。以此将堆外内存与GC相关联。


Reference静态块代码如下:
Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第8张图片

tryHandlerPending代码如下:
Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第9张图片

      存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。如果熬过了young gc,进入老生带之后,因为很小很难触发full gc,如果没有别的大块头进入老生代触发full gc,就会占着一大片堆外内存不释放。虽然申请内存并且不足时会调用Systen.gc(),但是他中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会抛出OOM异常。如果设置了-DisableExplicitGC禁止了system.gc(),就更难受了。所以,堆外内存还是调用Cleaner的clean方法主动回收比较好。


      Netty也提供了主动回收的release方法。
Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第10张图片
      此处用到了引用计数器,如果是2(引用计数为偶数,详情见博客-10),说明是最后的引用,尝试回收空间,并将其归零,如果不为2则修改其值,并判断是否需要回收内存空间。如果需要则调用deallocate()方法回收内存。

在这里插入图片描述

      经过一段比较长的调用,最终会调用CleanerJava6中的freeDirectBuffer0,通过反射获取cleaner对象并执行其clean方法。另外还有CleanerJava9的实现,使用Unsafe的invokeCleaner方法。

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第11张图片



noCleaner策略


      实现类为InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf,继承关系如下图:

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第12张图片

重写了分配内存,重新分配内寸和释放内存方法:

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第13张图片

分配内存时,调用PlatformDependent#allocateDIrectNoCleaner实现:
在这里插入图片描述

      最终实现是使用Unsafe分配内存后返回内存对象地址,并以此为参数反射调用DirectByteBuffer(long addr,int cap)构造方法创建实例。

Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第14张图片

      DirectByteBuffer(long addr,int cap)构造方法并不会申请内存,也不会创建Cleaner对象,只是做了基本参数的赋值。申请内存操作已经在allocateDirectNoCleaner中已经做过了。
Netty篇:ByteBuf之堆外内存与回收策略源码分析(非池化)_第15张图片

      noClean的release方法实现基本相同,只是在释放内寸的时候,实现不一样,直接用unsafe来释放内存。

在这里插入图片描述
在这里插入图片描述



你可能感兴趣的:(Netty篇)