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