现网日志打印内存泄漏

首先encode的内存泄漏好说,是因为在encode的时候,申请了一块直接内存的变量,用完后又没有release,这个非常好解决

 

decode的非常不好解决

从现网日志看,有接收到的数据缓存没有release的情况,但是很不好改,为啥

项目分了3层

T-连接设备-》A-解析数据-》S查询各种缓存,填充业务数据

A层时不时会打印这个decode的内存泄漏

首先A是直接复用了直接内存,没有拷贝

在上层业务处理上,用了线程池,每个线程添加一次引用,

又T-》A 这条路,n个设备公用一个通道,导致引起内存泄漏

 

 

 

可以在netty启动时候添加参数

ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED);

 

 

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机制之外,有自己的引用计数器和回收过程。

引用计数器
计数器基于 AtomicIntegerFieldUpdater,为什么不直接用AtomicInteger?因为ByteBuf对象很多,如果都把int包一层AtomicInteger花销较大,而AtomicIntegerFieldUpdater只需要一个全局的静态变量
所有ByteBuf的引用计数器初始值为1,当引用计数器为0,底下的buffer已被回收,即使ByteBuf对象还在,对它的各种访问操作都会抛出异常
调用release(),将计数器减1,等于零时, deallocate()被调用,各种回收
调用retain(),将计数器加1,即使ByteBuf在别的地方被人release()了,在本Class没喊cut之前,不要把它释放掉
由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极高时有性能的提升
————————————————
版权声明:本文为CSDN博主「布道」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/alex_xfboy/article/details/89643626

posted @ 2022-10-17 13:44  heroinss  阅读(337)  评论(0编辑  收藏  举报