记一次 netty 堆外内存泄漏排查

netty内存泄漏:

问题描述

测试环境netty服务日志提示有内存泄漏:

使用netty开发的聊天服务,运行一段时间后日志会显示

2022-06-17 10:35:53.488 ERROR 27253 --- [tLoopGroup-13-1] io.netty.util.ResourceLeakDetector       : LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records: 
Created at:
        io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:331)
        io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:185)
        io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:176)
        io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:113)
        io.netty.buffer.ByteBufUtil.readBytes(ByteBufUtil.java:430)
        io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder.decode(WebSocket08FrameDecoder.java:304)
        io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:489)
        io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:428)
        io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:265)
        io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
        io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
        io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:352)
        io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1421)
        io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:374)
        io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:360)
        io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:930)
        io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:163)
        io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:697)
        io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:632)
        io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:549)
        io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:511)
        io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:918)
        io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
        io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
        java.lang.Thread.run(Thread.java:748)

异常分析:

  • netty消息的读写使用的是ByteBuf,默认的实现是池化的直接内存,即PooledByteBuf。
  • PooledByteBuf的回收机制是使用的引用计数法,从 Netty 版本 4 开始,某些对象的生命周期由它们的引用计数管理,因此一旦不再使用(引用计数变成0),Netty 就可以将它们(或它们的共享资源)返回到对象池(或对象分配器)。
  • 引用计数的缺点是很容易泄漏引用计数的对象。因为 JVM 不知道 Netty 实现的引用计数,所以一旦JVM认为它们变得无法访问,即使它们的引用计数不为零,JVM也会自动对它们进行垃圾收集。这样就不会触发PooledByteBuf的回收机制。一个对象一旦被垃圾回收就不能复活,因此不能返回到它来自的池中,从而会产生内存泄漏。

简单来说,就是程序中的某个对象的引用计数出了问题,对象被垃圾回收器回收的时候,引用计数不为0,导致其占用的直接内存未被释放,从而会产生内存泄漏。


更改日志级别:

这个错误日志是由netty的内存泄漏检测机制提示的,Netty的内存泄漏检测分为四个级别:

  • DISABLED - 完成禁止检测内存泄漏,这个是不推荐。
  • SIMPLE - 如果buffer中出现1%的内存泄漏,打印错误日志,就是上面那样。但是日志 是看不到详细在什么位置出现了内存泄漏。鉴于性能考虑,这个是在生产环境中默认使用。
  • ADVANCED - 如果buffer的1%出现内存泄漏,打印错误,并且输出详细的内存泄漏信息。
  • PARANOID - 这个和ADVANCED输出的内容是一样的,但它会对每一次请求的buffer做检查。很适用于调试和单元测试。

在代码中更改日志级别或者在启动参数中指定日志级别

## 程序中
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.PARANOID);
## 或者java 启动命令中增加参数
-D io.netty.leakDetectionLevel=PARANOID

定位问题:

更改日志级别后,会在日志中打印更详细的信息,我们代码中有一段

//      处理 IllegalReferenceCountException 异常 .
        ByteBuf echoMsg = msg.content();
        echoMsg.retain();

之前的业务逻辑中,为了处理一个IllegalReferenceCountException,手工给引用计数+1了,后续代码变更,没有去掉这个逻辑,导致内存泄漏

结论:

去掉就好了,哈哈

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值