有过痛苦的经历,特别能写出深刻的文章 —— 凯尔文. 肖
直接内存是IO框架的绝配,但直接内存的分配销毁不易,所以使用内存池能大幅提高性能,也告别了频繁的GC。但,要重新培养被Java的自动垃圾回收惯坏了的惰性。
Netty有一篇必读的文档 官方文档翻译:引用计数对象 ,在此基础上补充一些自己的理解和细节。
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,情况更为复杂。
在C时代,我们喜欢让malloc和free成对出现,而在Netty里,因为Handler链的存在,ByteBuf经常要传递到下一个Hanlder去而不复还,所以规则变成了谁是最后使用者,谁负责释放。
另外,更要注意的是各种异常情况,ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。
在AbstractNioByteChannel.NioByteUnsafe.read() 处创建了ByteBuf并调用 pipeline.fireChannelRead(byteBuf) 送入Handler链。
根据上面的谁最后谁负责原则,每个Handler对消息可能有三种处理方式
假设每一个Handler都把消息往下传,Handler并也不知道谁是启动Netty时所设定的Handler链的最后一员,所以Netty在Handler链的最末补了一个TailHandler,如果此时消息仍然是ReferenceCounted类型就会被release掉。
要发送的消息由应用所创建,并调用 ctx.writeAndFlush(msg) 进入Handler链。在每个Handler中的处理类似InBound Message,最后消息会来到HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。
多层的异常处理机制,有些异常处理的地方不一定准确知道ByteBuf之前释放了没有,可以在释放前加上引用计数大于0的判断避免释放失败;
有时候不清楚ByteBuf被引用了多少次,但又必须在此进行彻底的释放,可以循环调用reelase()直到返回true。
所谓内存泄漏,主要是针对池化的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被创建和访问的地方。
每当各种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