Netty|Netty -- 内存管理

  • JVM在常规IO时,需要将堆内存中的Buffer复制一份到直接内存中,操作系统内核才能接管进行接下来的通信,Netty为了降低这个复制的开销,设计实现了一种IO时zero-copy内存的机制。
  • 【Netty|Netty -- 内存管理】对于一个Buffer来说,Netty从复用方式以及申请方式两个角度进行分类
    1. 复用方式:采用了Pool机制(申请直接内存比申请堆内存开销要大),根据内存空间在使用结束以后是否立即归还给操作系统或者JVM分为Pooled以及Unpooled
    2. 申请方式:从操作系统申请直接内存为Direct,从JVM申请堆内存为Heap
  • Netty内存管理,关键在于ByteBuf类的四种实现,对应于Buffer两种复用方式和两种申请方式的迪卡尔积
    1. UnpooledHeapByteBuf
    2. UnpooledDirectByteBuf
    3. PooledHeapByteBuf
    4. PooledDirectByteBuf
    5. 对于使用堆内存的方式,此时内存的分配与回收都由GC来决定
    6. 如果使用了直接内存,需要使用者利用Netty的引用计数机制来显式的维护内存的释放
  • 引用计数
    1. 计数器基于 AtomicIntegerFieldUpdater
    2. 所有ByteBuf的引用计数器初始值为1
    3. 调用release(),将计数器减1,等于零时将会被回收
    4. 调用retain(),将计数器加1
    5. duplicate(),slice()和order()等操作会获得子Buff,子缓冲没有自己的引用计数,而是共享父缓冲的引用计数。如果要传递(pass)一个子缓冲给程序中的其他组件的话,得先调用retain()。
  • Zero-copy
    1. Netty的zero-copy机制是基于直接内存PooledDirectByteBuf实现
    2. PooledDirectByteBuf使用了JDK的DirectBuffer类,这个类可以申请直接内存,并保存了直接内存地址以及Cleaner(释放时调用clean方法,即free掉内存)等信息
    3. 当堆内的DirectBuffer类被回收时,直接内存也会被回收
    4. Netty维护自身的引用计数机制的原因:如果IO时创建的PooledDirectByteBuf对象熬过了Young gc阶段进入了老年代,当Full gc的频率较低(PooledDirectByteBuf对象在堆内存中的占用很小)时,老年代中可能会累积大量的未被回收的PooledDirectByteBuf对象,这也代表着堆外会有许多直接内存没有被释放掉,此时如果继续申请直接内存,申请额度超限会主动触发System.gc()来救场,但是如果gc时间超过等待时间(100ms),或者显式的gc被禁用,那么就会导致OOM,因此依靠JVM的gc机制来实现直接内存的释放是一件非常不靠谱的事情,Netty便自行维护了一套引用计数的机制。
  • 显式的维护内存的释放
    1. Netty中是通过Handler链来处理各类事件
    2. 接收消息时,在前面的Handler对ByteBuf进行处理以后需要调用ctx.fireChannelRead(msg)把消息继续传递给下一个Handler,因此ByteBuf的释放需要由Handler链中最后一个Handler(可能产生异常)来显式的释放,方式为:ReferenceCountUtil.release()
    3. 发送消息时,由于写入到Buffer的操作最终由Netty来完成,因此释放操作也是Netty自行维护
Netty官方文档
官方文档中文版
Netty自行维护引用计数的讨论
Netty资料1
Netty资料2

    推荐阅读