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了,后续代码变更,没有去掉这个逻辑,导致内存泄漏
结论:
去掉就好了,哈哈