上文描述过,ByteBuf根据内存分可以分为堆内存和堆外内存实现,此处以非池化实现介绍,两个重要子类是UnpooledHeapByteBuf和UnpooledDirectByteBuf,因为堆外内存和堆内存除了内存分配之外,其他实现十分相似,先简单描述下堆内存实现,然后重点分析堆外内存的分配及回收策略
ByteBuf的堆内存主要由子类UnpooledHeapByteBuf实现,UnpooledHeapByteBuf是基于堆内存进行内存分配的字节缓冲区,它没有基于对象池技术实现,每次I/O的读写都会创建一个新的UnpooledHeapByteBuf。
基础属性如下:
聚合了一个ByteBufAllocator用于UnpooledHeapByteBuf的内存分配,一个byte数组作为缓冲区,最后定义了一个ByteBuffer类型的tmpNioBuf变量用于实现Netty ByteBuf到JDK NIO ByteBUffer的转换。
首先对新容量进行合法性校验,如果大于容量上限或者小于0,则抛出异常。
判断新的容量是否大于当前的缓冲区容量,大于则需要进行动态扩展。创建新的缓冲区字节数组,然后System.arraycopy进行内存复制,setArray替换旧的字节数组,释放旧内存。
将ByteBuf转换为ByteBuffer,使用了NIO的ByteBuffer提供的wrap方法,将byte数组转换为ByteBuffer对象。
入口在UnpooledByteBufAllocator#newDirectBuffer方法。Netty提供了两种内存策略,一种是使用java.nio.DirectByteBuffer实现,另一种是noCleaner策略实现。用不同的对象实现,均继承自UnpooledUnsafeDirectByteBuf。
实现类为InstrumentedUnpooledUnsafeDirectByteBuf,继承关系如下图:
分配内存时,直接调用java.nio.DirectByteBuffer实现。
DIrectByteBuffer的内存分配代码如下:
向Bits类申请内存,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限,堆外内存的限额默认与堆内内存(由-XMX 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。
跟踪reserveMemory:
如果已经超限,会主动执行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相关联。
存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。如果熬过了young gc,进入老生带之后,因为很小很难触发full gc,如果没有别的大块头进入老生代触发full gc,就会占着一大片堆外内存不释放。虽然申请内存并且不足时会调用Systen.gc(),但是他中断整个进程,然后它让当前线程睡了整整一百毫秒,而且如果gc没在一百毫秒内完成,它仍然会抛出OOM异常。如果设置了-DisableExplicitGC禁止了system.gc(),就更难受了。所以,堆外内存还是调用Cleaner的clean方法主动回收比较好。
Netty也提供了主动回收的release方法。
此处用到了引用计数器,如果是2(引用计数为偶数,详情见博客-10),说明是最后的引用,尝试回收空间,并将其归零,如果不为2则修改其值,并判断是否需要回收内存空间。如果需要则调用deallocate()方法回收内存。
经过一段比较长的调用,最终会调用CleanerJava6中的freeDirectBuffer0,通过反射获取cleaner对象并执行其clean方法。另外还有CleanerJava9的实现,使用Unsafe的invokeCleaner方法。
实现类为InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf,继承关系如下图:
重写了分配内存,重新分配内寸和释放内存方法:
分配内存时,调用PlatformDependent#allocateDIrectNoCleaner实现:
最终实现是使用Unsafe分配内存后返回内存对象地址,并以此为参数反射调用DirectByteBuffer(long addr,int cap)构造方法创建实例。
DirectByteBuffer(long addr,int cap)构造方法并不会申请内存,也不会创建Cleaner对象,只是做了基本参数的赋值。申请内存操作已经在allocateDirectNoCleaner中已经做过了。
noClean的release方法实现基本相同,只是在释放内寸的时候,实现不一样,直接用unsafe来释放内存。