Netty的资源泄露探测器:ResourceLeakDetector

因为 Netty 大量使用 ByteBuf,如果 ByteBuf 出现泄露,则服务很容易出现 OOM。Netty 中的ResourceLeakDetector就是为解决该问题而生,它记录 Netty 使用的各种 ByteBuf 的使用,能对占用资源的对象进行监控。无论是 Pooled 还是 Unpooled,无论是 Direct 还是 Heap,所有的 ByteBuf 都要被 ResourceLeakDetector 记录起来。从而在开发者出现忘记为 ByteBuf 调用 release 的时候,通过日志告知开发者有泄露,要求开发者来排查问题。

为了解决网卡内核态与应用用户态之间零复制,Netty支持海量连接,高性能而堆外内存是重要贡献之一。它带来的好处有:无GC、不受堆内存大小限制。

JVM堆內堆外内存拷贝

 堆内内存堆外内存
底层实现JVM 内存unsafe.allocateMemory(size)分配直接内存
大小限制-Xms-Xmx 配置的 JVM 内存相关可以通过 -XX:MaxDirectMemorySize 参数从 JVM 层面去限制,同时受到机器虚拟内存(说物理内存不太准确)的限制
垃圾回收

当 DirectByteBuffer 不再被使用时,会出发内部 cleaner 的钩子,

保险起见,可以考虑手动回收:((DirectBuffer) buffer).cleaner().clean();

拷贝形态用户态<->内核态内核态

主要依赖UNSAFE 魔法类实现的:

//无论是堆内和堆外都可以实现内存之间的拷贝
ByteBuffer buffer = ByteBuffer.allocateDirect(4 * 1024 * 1024);
long addresses = ((DirectBuffer) buffer).address();
byte[] data = new byte[4 * 1024 * 1024];
UNSAFE.copyMemory(data, 16, null, addresses, 4 * 1024 * 1024);

DirectByteBuffer

它首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限,而堆外内存的限额默认与堆内内存(由-XMX 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。

采用Non-direct ByteBuffer的流程:网络 –> 临时的DirectByteBuffer –> 应用 Non-direct ByteBuffer –> 临时的Direct ByteBuffer –> 网络

采用Direct ByteBuffer的流程:网络 –> 应用 Direct ByteBuffer –> 网络

它免去中间交换的内存拷贝, 提升IO处理速度,也就是常说的zero-copy!通常,Zero-Copy技术省去了将操作系统的read buffer拷贝到程序的buffer,以及从程序buffer拷贝到socket buffer的步骤,直接将read buffer拷贝到socket buffer。更多

  • 如果已经超限,会主动执行Sytem.gc(),期待能主动回收一点堆外内存。然后休眠一百毫秒,看看totalCapacity降下来没有,如果内存还是不足,就抛出大家最头痛的OOM异常
  • 如果额度被批准,就调用大名鼎鼎的sun.misc.Unsafe(C++实现)去分配内存,返回内存基地址,它有标准的malloc,然后再调一次Unsafe把这段内存给清零

JDK7开始,DirectByteBuffer分配内存时默认已不做分页对齐,不会再每次分配并清零 实际需要+分页大小(4k)的内存,这对性能应有较大提升,所以Oracle专门写在了Enhancements in Java I/O里。

但存在于堆内的DirectByteBuffer对象很小,只存着基地址和大小等几个属性,和一个Cleaner,但它代表着后面所分配的一大段内存。DirectByteBuffer内部的Cleaner的作用是清理动作(clean方法),清理执行时实际调用的是被绑定的Deallocator类(降低Bits里的totalCapacity,并调用Unsafe调free去释放内存),这个类可被重复执行,释放过了就不再释放 。简单的讲就是,堆内的DirectByteBuffer对象被GC时,它背后的堆外内存也会被回收。

GC历史回顾

当新生代满了,就会发生young gc;如果此时对象还没失效,就不会被回收;撑过几次young gc后,对象被迁移到老生代;当老生代也满了,就会发生full gc。

原来JDK除了StrongReference,SoftReference 和 WeakReference之外,还有一种PhantomReference,Phantom是幻影的意思,Cleaner就是PhantomReference的子类。

带来新问题

因为DirectByteBuffer本身的个头很小,只要熬过了young gc,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发full gc,如果没有别的大块头进入老生代触发full gc,就一直在那耗着,占着一大片堆外内存不释放。

当GC时发现它除了PhantomReference外已不可达(持有它的DirectByteBuffer失效了),就会把它放进 Reference类pending list静态变量里。然后另有一条ReferenceHandler线程,名字叫 "Reference Handler"的,关注着这个pending list,如果看到有对象类型是Cleaner,就会执行它的clean(),其他类型就放入应用构造Reference时传入的ReferenceQueue中,这样应用的代码可以从Queue里拖出这些理论上已死的对象,这是一种比finalizer更轻量更好的机制。

怎么办呢?只能system.gc()来救场,万一设置了-DisableExplicitGC禁止了system.gc(),那就恐怖了。

Netty的ByteBuf

Netty骄傲的地方就是内存池的管理,从4.0版本开始经常变改,虽是直接内存IO框架的绝配,但直接内存的分配销毁不易,所以使用内存池能大幅提高性能,也告别了频繁的GC。Netty里四种主力的ByteBuf:

  • UnpooledHeapByteBuf ,内部的byte[]能够依赖JVM GC自然回收,每次I/O读写都会创建一个新的UnpooledHeapByteBuf,频繁进行大内存分配和回收
  • UnpooledDirectByteBuf ,内部是DirectByteBuffer,但相比于堆内内存申请和释放,成本要高一些。
  • PooledHeapByteBuf ,必须要主动将用完的byte[]放回池里,否则内存就要爆掉
  • PooledDirectByteBuf ,必须要主动将用完的ByteBuffer放回池里,否则内存就要爆掉

注:《Netty权威指南》说 PooledDirectByteBuf 对DirectByteBuffer进行重用性能提升了23倍

Netty ByteBuf需要在JVM的GC机制之外,有自己的引用计数器和回收过程。

引用计数器

  1. 计数器基于 AtomicIntegerFieldUpdater,为什么不直接用AtomicInteger?因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量
  2. 所有ByteBuf的引用计数器初始值为1,当引用计数器为0,底下的buffer已被回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常
  3. 调用release(),将计数器减1,等于零时, deallocate()被调用,各种回收
  4. 调用retain(),将计数器加1,即使ByteBuf在别的地方被人release()了,在本Class没喊cut之前,不要把它释放掉
  5. 由duplicate(), slice()和order()所衍生的ByteBuf,与原对象共享底下的buffer,也共享引用计数器,所以它们经常需要调用retain()来显示自己的存在

回收过程

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

InBound Message

在AbstractNioByteChannel.NioByteUnsafe.read() 处创建了ByteBuf并调用 pipeline.fireChannelRead(byteBuf) 送入Handler链,因此,每个Handler对消息可能有三种处理方式:

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

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

OutBound Message

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

异常时如何释放?

各种异常情况下ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。多层的异常处理机制,可以在释放前加上引用计数大于0的判断避免释放失败,也可以可以循环调用reelase()直到返回true。

ResourceLeakDetector

所谓内存泄漏,主要是针对池化的ByteBuf,ByteBuf对象被JVM GC掉之前,没有调用release()把底下的DirectByteBuffer或byte[]归还到池里,会导致池越来越大。

它支持的设置参数:

  • SIMPLE,默认等级,告诉我们取样的1%的ByteBuf是否发生了泄露,但总共一次只打印一次,看不到就没有了,wrapper只在执行release()时调用Reference.clear()
  • DISABLED,禁用,完全禁止泄露检测,省点消耗
  • ADVANCED,高级,告诉我们取样的1%的ByteBuf发生泄露的地方。每种类型的泄漏(创建的地方与访问路径一致)只打印一次,对性能有影响
  • PARANOID,偏执,跟高级选项类似,但此选项检测所有ByteBuf,而不仅仅是取样的那1%。对性能有绝大的影响

ResourceLeakDetector的版本也一直在更新

  • 2016 年 12 月的时候,Netty 对 ResourceLeakDetector 做了改动,修复了一个隐藏很久的 Bug,很多很多年都没有被发现。Bug 的现象是即使记得调用了 LeakAwareResource 的 close,释放了 Resource,但 Netty 的 ReferenceDetector 还是会错误报出发现内存泄露,这里所说的老版本代码基于 v4.0.28
  • 在 v4.1.9 后,ResourceLeakDetector 又做了很多性能上的优化,只是这里为了不引入太多东西看着复杂

该如何设置呢?

建议要对使用的特性进行充足的单元测试,同时将内存泄漏检查级别开到最高,然后每个用例执行完就System.gc()一次,关注的logger有没有输出memory leak信息(log有出现 "LEAK: "字样)。

  • 功能测试时,最好开着"-Dio.netty.leakDetectionLevel=paranoid"
  • 生产环境时,最好加上"-Dio.netty.leakDetectionLevel=disabled”把检测关掉,TPS极高时有性能的提升

引用资料

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值