文章目录
- 前言
- ByteBuf
- 问题分析
- 解决方法
- 单个方法中
- ChannelHandler链中
前言
项目中使用Netty接收设备传递的数据时,查看日志发现有时会报错:LEAK: ByteBuf.release() was not called before it's garbage-collected.
根据提示也可以看出是使用ByteBuf时,没有及时调用ByteBuf.release() 而引发的内存泄漏。
以下为日志中的报错信息:
2024-12-18 18:18:05.694 ERROR 841 --- [ntLoopGroup-5-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.newHeapBuffer(PooledByteBufAllocator.java:332)
io.netty.buffer.AbstractByteBufAllocator.heapBuffer(AbstractByteBufAllocator.java:168)
io.netty.buffer.AbstractByteBufAllocator.heapBuffer(AbstractByteBufAllocator.java:159)
com.lc.netty.ListenChannelInitHandler.initChannel(ListenChannelInitHandler.java:141)
com.lc.netty.ListenChannelInitHandler.initChannel(ListenChannelInitHandler.java:26)
io.netty.channel.ChannelInitializer.initChannel(ChannelInitializer.java:129)
io.netty.channel.ChannelInitializer.handlerAdded(ChannelInitializer.java:112)
io.netty.channel.AbstractChannelHandlerContext.callHandlerAdded(AbstractChannelHandlerContext.java:964)
io.netty.channel.DefaultChannelPipeline.callHandlerAdded0(DefaultChannelPipeline.java:610)
io.netty.channel.DefaultChannelPipeline.access$100(DefaultChannelPipeline.java:46)
io.netty.channel.DefaultChannelPipeline$PendingHandlerAddedTask.execute(DefaultChannelPipeline.java:1474)
io.netty.channel.DefaultChannelPipeline.callHandlerAddedForAllHandlers(DefaultChannelPipeline.java:1126)
io.netty.channel.DefaultChannelPipeline.invokeHandlerAddedIfNeeded(DefaultChannelPipeline.java:651)
io.netty.channel.AbstractChannel$AbstractUnsafe.register0(AbstractChannel.java:503)
io.netty.channel.AbstractChannel$AbstractUnsafe.access$200(AbstractChannel.java:416)
io.netty.channel.AbstractChannel$AbstractUnsafe$1.run(AbstractChannel.java:475)
io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:163)
io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:416)
io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:515)
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)
ByteBuf
Netty中ByteBuf按是否使用了池化技术,ByteBuf分为两类,
- 一类是非池化的ByteBuf,包括UnpooledHeapByteBuf、UnpooledDirectByteBuf等等,每次I/O读写都会创建一个新ByteBuf,频繁进行大块内存的分配和回收对性能有一定影响,非池化的ByteBuf可以通过JVM GC自动回收,也推荐手动回收UnpooledDirectByteBuf等使用堆外内存的ByteBuf;
- 另一类是池化的ByteBuf,包括pooledHeapByteBuf、pooledDirectByteBuf等等,其先申请一块大内存池,在内存池中分配空间,对于这种应用级别的内存二次分配,就需要手动对池化的ByteBuf进行释放,否则就有可能出现内存泄露的问题。
问题分析
出现这个问题是因为程序中使用了池化的ByteBuf(PooledByteBuf)。而PooledByteBuf的内存空间是从内存池中分配的,PooledByteBuf通过引用计数来管理内存。当创建一个池化的ByteBuf时,其引用计数初始化为 1。每次将ByteBuf传递给其他对象或在某个地方被引用时,引用计数应该相应增加(通常在 Netty 内部机制中自动处理)。
当不再需要这个ByteBuf时,应该调用release()方法来减少引用计数(每次调用release()-1)。,减小到0时就会将内存归还在内存池中。
如果没有调用release(),引用计数不会减少到 0,那么内存将不会被释放回内存池,即使应用程序不再使用这个ByteBuf,它所占用的内存仍然被保留,从而导致内存泄漏。
解决方法
单个方法中
既然通过日志已经知道了内存泄漏的位置,接下来就需要再使用完手动调用其release方法释放
ByteBuf buffer = Unpooled.copiedBuffer("test".getBytes());
try {
...
} catch (Exception e) {
buffer.release();
log.error("数据异常:", e);
}finally {
buffer.release();
}
ChannelHandler链中
因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性。
所以规则变成了 谁是最后使用者,谁负责释放
。另外,更要注意的是各种 异常情况,ByteBuf没有成功传递到下一个Hanlder,还在自己地界里的话,一定要进行释放。
- 入站 ByteBuf 处理原则
- 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release
- 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release
- 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release
- 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
- 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
- 出站 ByteBuf 处理原则
- 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release
- 异常处理原则
- 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true
相关文章:
Netty-组件ByteBuf