Netty之有效规避内存泄漏

有过痛苦的经历,特别能写出深入的文章 —— 凯尔文. 肖html

直接内存是IO框架的绝配,但直接内存的分配销毁不易,因此使用内存池能大幅提升性能,也告别了频繁的GC。但,要从新培养被Java的自动垃圾回收惯坏了的惰性。app

Netty有一篇必读的文档 官方文档翻译:引用计数对象 ,在此基础上补充一些本身的理解和细节。框架

 

1.为何要有引用计数器

Netty里四种主力的ByteBuf,
其中UnpooledHeapByteBuf 底下的byte[]可以依赖JVM GC天然回收;而UnpooledDirectByteBuf底下是DirectByteBuffer,如Java堆外内存扫盲贴所述,除了等JVM GC,最好也能主动进行回收;而PooledHeapByteBuf 和 PooledDirectByteBuf,则必需要主动将用完的byte[]/ByteBuffer放回池里,不然内存就要爆掉。因此,Netty ByteBuf须要在JVM的GC机制以外,有本身的引用计数器和回收过程。性能

一下又回到了C的冰冷时代,本身malloc对象要本身free。 但和C时代又不彻底同样,内有引用计数器,外有JVM的GC,状况更为复杂。测试

 

2. 引用计数器常识

  • 计数器基于 AtomicIntegerFieldUpdater,为何不直接用AtomicInteger?由于ByteBuf对象不少,若是都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只须要一个全局的静态变量。
  • 全部ByteBuf的引用计数器初始值为1。
  • 调用release(),将计数器减1,等于零时, deallocate()被调用,各类回收。
  • 调用retain(),将计数器加1,即便ByteBuf在别的地方被人release()了,在本Class没喊cut以前,不要把它释放掉。
  • 由duplicate(), slice()和order()所衍生的ByteBuf,与原对象共享底下的buffer,也共享引用计数器,因此它们常常须要调用retain()来显示本身的存在。
  • 当引用计数器为0,底下的buffer已被回收,即便ByteBuf对象还在,对它的各类访问操做都会抛出异常

 

3.谁来负责Release

在C时代,咱们喜欢让malloc和free成对出现,而在Netty里,由于Handler链的存在,ByteBuf常常要传递到下一个Hanlder去而不复还,因此规则变成了谁是最后使用者,谁负责释放spa

另外,更要注意的是各类异常状况,ByteBuf没有成功传递到下一个Hanlder,还在本身地界里的话,必定要进行释放.net

3.1 InBound Message

在AbstractNioByteChannel.NioByteUnsafe.read() 处建立了ByteBuf并调用 pipeline.fireChannelRead(byteBuf) 送入Handler链。翻译

根据上面的谁最后谁负责原则,每一个Handler对消息可能有三种处理方式日志

  • 对原消息不作处理,调用 ctx.fireChannelRead(msg)把原消息往下传,那不用作什么释放。
  • 将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉。
  • 若是已经再也不调用ctx.fireChannelRead(msg)传递任何消息,那更要把原消息release掉

假设每个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,因此Netty在Handler链的最末补了一个TailHandler,若是此时消息仍然是ReferenceCounted类型就会被release掉。
 netty

3.2 OutBound Message

要发送的消息由应用所建立,并调用 ctx.writeAndFlush(msg) 进入Handler链。在每一个Handler中的处理相似InBound Message,最后消息会来到HeadHandler,再通过一轮复杂的调用,在flush完成后终将被release掉

 

3.3 异常发生时的释放

多层的异常处理机制,有些异常处理的地方不必定准确知道ByteBuf以前释放了没有,能够在释放前加上引用计数大于0的判断避免释放失败;

有时候不清楚ByteBuf被引用了多少次,但又必须在此进行完全的释放,能够循环调用reelase()直到返回true

 

4. 内存泄漏检测

所谓内存泄漏,主要是针对池化的ByteBuf。ByteBuf对象被JVM GC掉以前,没有调用release()把底下的DirectByteBuffer或byte[]归还到池里,会致使池愈来愈大。而非池化的ByteBuf,即便像DirectByteBuf那样可能会用到System.gc(),但终归会被release掉的,不会出大事。

Netty担忧你们不当心就搞出个大新闻来,所以提供了内存泄漏的监测机制。

Netty默认会从分配的ByteBuf里抽样出大约1%的来进行跟踪。若是泄漏,会有以下语句打印:

 

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

这句话报告有泄漏的发生,提示你用-D参数,把防漏等级从默认的simple升到advanced,就能具体看到被泄漏的ByteBuf被建立和访问的地方。

  • 禁用(DISABLED) - 彻底禁止泄露检测,省点消耗。
  • 简单(SIMPLE) - 默认等级,告诉咱们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了。
  • 高级(ADVANCED) - 告诉咱们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(建立的地方与访问路径一致)只打印一次。对性能有影响。
  • 偏执(PARANOID) - 跟高级选项相似,但此选项检测全部ByteBuf,而不只仅是取样的那1%。对性能有绝大的影响。

实现细节

每当各类ByteBufAllocator 建立ByteBuf时,都会问问是否须要采样,Simple和Advanced级别下,就是以113这个素数来取模(害我看文档的时候还在瞎担忧,1%,万一泄漏的地方有所规律,恰好躲过了100这个数字呢,好比都是3倍数的),命中了就建立一个Java堆外内存扫盲贴里说的PhantomReference。而后建立一个Wrapper,包住ByteBuf和Reference。

simple级别下,wrapper只在执行release()时调用Reference.clear(),Advanced级别下则会记录每个建立和访问的动做。

当GC发生,尚未被clear()的Reference就会被JVM放入到以前设定的ReferenceQueue里。

在每次建立PhantomReference时,都会顺便看看有没有由于忘记执行release()把Reference给clear掉,在GC时被放进了ReferenceQueue的对象,有则以 "io.netty.util.ResourceLeakDetector”为logger name,写出前面例子里的Error级别的日日志。顺便说一句,Netty能自动匹配日志框架,先找Slf4j,再找Log4j,最后找JDK logger。

值得说三遍的事

必定要盯紧log里有没有出现 "LEAK: "字样,由于simple级别下它只会出现一次,因此不要依赖本身的眼睛,要依赖grep。若是出现了,并且你用的是PooledBuf,那必定是问题,不要有任何的侥幸,马上用"-Dio.netty.leakDetectionLevel=advanced" 再跑一次,看清楚它建立和访问的地方。

功能测试时,最好开着"-Dio.netty.leakDetectionLevel=paranoid"。

可是,怎么测试均可能存在没有覆盖到的分支。若是内存尚够,能够适当把-XX:MaxDirectMemorySize 调大,反正只是max,平时也不会真用了你的。而后监控其使用量,及时报警。

 
文章持续修订,转载请保留原连接: http://calvin1978.blogcn.com/articles/netty-leak.html

相关文章
相关标签/搜索