Netty中ByteBuf内存泄露及释放解析
近日在使用Netty框架开发程序中出现了内存泄露的问题,百度加调试了一番,做个整理。
1. ByteBuf分类、回收及使用场景
Netty中按是否使用了池化技术,ByteBuf分为两类,一类是非池化的ByteBuf,包括UnpooledHeapByteBuf、UnpooledDirectByteBuf等等,每次I/O读写都会创建一个新ByteBuf,频繁进行大块内存的分配和回收对性能有一定影响,非池化的ByteBuf可以通过JVM GC自动回收,也推荐手动回收UnpooledDirectByteBuf等使用堆外内存的ByteBuf;另一类是池化的ByteBuf,包括pooledHeapByteBuf、pooledDirectByteBuf等等,其先申请一块大内存池,在内存池中分配空间,对于这种应用级别的内存二次分配,就需要手动对池化的ByteBuf进行释放,否则就有可能出现内存泄露的问题。
ByteBuf 该如何选择: 一般业务数据的内存分配选用Java堆内存UnpooledHeapByteBuf,其实现简单,回收快,不会出现内存管理问题;对于I/O数据的内存分配一般选用池化的直接内存PooledDirectByteBuf,避免Java堆内存到直接内存的拷贝,但使用池化ByteBuf时切记自己分配的内存一定要在用完后手动释放。
Netty的接收和发送ByteBuf采用的DirectBuffers,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(heap buffers)进行socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入socket中。相比堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
2. ByteBuf的计数器实现
那对于池化的ByteBuf在使用完释放回池中时,如何知道自己被引用多少次,并且在没有其他引用的时候被释放呢?ByteBuf的具备实现类都继承了AbstractReferenceCountedByteBuf类,该类实现了对计数器的操作功能。当某一操作使得ByteBuf的引用增加时,调用retain()函数,使计数器的值原子增加,当某一操作使得ByteBuf的引用减少时,调用release()函数,使计数器的值原子减少,减少到0便会触发回收操作。关于AbstractReferenceCountedByteBuf类的源码分析,请见Netty框架AbstractReferenceCountedByteBuf 源码分析。
3. ByteBuf计数器的调用场景
(1)当一个ByteBuf新建时,或从另一个ByteBuf创建出独立个体时(比如copy(),readBytes(int length)),新的ByteBuf在初始化的时候,自己的计数器也会初始化。这种ByteBuf使用结束后,要主动释放
(2)有些ByteBuf从另一个ByteBuf衍生出来时(比如decode(),retainedSlice(index, length)),底层共用同一个buffer,也会调用retain()函数,来使得计数器增加。使用完毕也要主动释放。
(3)有些ByteBuf从另一个ByteBuf衍生出来时(比如duplicate(), slice(), order()),底层与父类ByteBuf共用一个buffer,其没有自己的计数器,共用父类ByteBuf的计数器,计数器也不会增加。因此,如果要把这些衍生ByteBuf传递给其他函数时,必须要主动调用retain()函数,并在本函数释放父类ByteBuf,在下一个函数里释放衍生ByteBuf。如下面代码

ByteBuf parent = ctx.alloc().directBuffer(512);
parent.writeBytes(...); try {
while (parent.isReadable(16)) {
ByteBuf derived = parent.readSlice(16);
derived.retain();
process(derived);
}
} finally {
parent.release();
}
... public void process(ByteBuf buf) {
...
buf.release();
}

以我遇到的内存泄漏的场景为例
(1)readBytes(int length)函数可能会调用到如下代码,新生成一个ByteBuf

@Override
protected PooledByteBuf<ByteBuffer> newByteBuf(int maxCapacity) {
if (HAS_UNSAFE) {
return PooledUnsafeDirectByteBuf.newInstance(maxCapacity);
} else {
return PooledDirectByteBuf.newInstance(maxCapacity);
}
}

(2)ByteBuf in = (ByteBuf) super.decode(ctx,inByteBuf) 调用decode函数时,会调用到buffer.retainedSlice(index, length)函数,会返回原ByteBuf的一个片段,同时使原ByteBuf的计数器增加。它们底层共用同一个buffer,修改一个会影响另一个。

final <U extends AbstractPooledDerivedByteBuf> U init(
AbstractByteBuf unwrapped, ByteBuf wrapped, int readerIndex, int writerIndex, int maxCapacity) {
wrapped.retain(); // Retain up front to ensure the parent is accessible before doing more work.
parent = wrapped;
rootParent = unwrapped;
......
}

4. 谁来释放ByteBuf
最基本的规则是谁最后访问ByteBuf,谁最后负责释放。需注意的是:
(1)发送组件将ByteBuf传递给接收组件,发送组件一般不负责释放,由接收组件释放;
(2)如果一个组件除了接收处理ByteBUf,而不做其他操作(比如再传给其他组件),这个组件负责释放ByteBuf。
例如
| Action | Who should release? |
Who released? |
1. main() creates buf |
buf→main() |
main() releases buf |
2. main() calls a() with buf |
buf→a() |
a() releases buf |
3. a() returns buf merely. |
buf→main() |
main() releases buf |
4. main() calls b() with buf |
buf→b() |
b() releases buf |
5. b() returns the copy of buf |
buf→b() |
b() releases buf, |
6. main() calls c() with copy |
copy→c() |
c() releases copy |
7. c() swallows copy |
copy→c() |
c() releases copy |
5. 在 ChannelHandler负责链中,如何释放
(1)在Inbound messages中
a. 如果ChannelHandler中,只有处理ByteBuf的操作,不会调ctx.fireChannelRead(buf)把ByteBuf传递下去,那就要在这个ChannelHandler中释放ByteBuf。
b. 如果ChannelHandler中,会调ctx.fireChannelRead(buf)把ByteBuf传递给下一个ChannelHandler,那在当前ChannelHandler中不需要释放ByteBuf,由最后一个使用该ByteBuf的ChannelHandler释放。
c. 如果处理的ByteBuf是由decode()等会增加计数器的操作生成的,不再传递时,ByteBuf也要释放。
d. 如果不确定要不要释放,或者简化释放的过程,可以调用ReferenceCountUtil.release(ByteBuf)函数。
e. 也可以把ChannelHandler都承继自SimpleChannelInboundHandler虚类,该类会在channelRead函数中调用ReferenceCountUtil.release(msg)来帮助释放ByteBuf,如下代码所示,channelRead0(ctx, imsg)是一个虚函数,子类实现channelRead0函数用来完成处理逻辑。

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
boolean release = true;
try {
if (acceptInboundMessage(msg)) {
@SuppressWarnings("unchecked")
I imsg = (I) msg;
channelRead0(ctx, imsg);
} else {
release = false;
ctx.fireChannelRead(msg);
}
} finally {
if (autoRelease && release) {
ReferenceCountUtil.release(msg);
}
}
}

(2)在Outbound messages中
Outbound messages中的ByteBuf都是由应用程序产生的,由Netty负责释放。
6. 内存泄露检测
使用引用计数的缺点在于容易产生内存泄露,因为JVM不知道引用计数的存在。当一个对象不可达时,JVM可能会收回该对象,但这个对象的引用计数可能还不是0,这就导致该对象从池里分配的空间不能归还到池里,从而导致内存泄露。
Netty提供了一种内存泄露检测机制,可以通过配置参数不同选择不同的检测级别,参数设置为java -Dio.netty.leakDetection.level=advanced
DISABLED:完全禁用内存泄露检测,不推荐SIMPLE:抽样1%的ByteBuf,提示是否有内存泄露ADVANCED:抽样1%的ByteBuf,提示哪里产生了内存泄露PARANOID:对每一个ByteBu进行检测,提示哪里产生了内存泄露
我在测试时,直接提示了ByteBuf内存泄露的位置,如下,找到自己程序代码,看哪里有新生成的ByteBuf对象没有释放,主动释放一下,调用对象的release()函数,或者用工具类帮助释放ReferenceCountUtil.release(msg)。

2020-06-12 17:04:41.242 [nioEventLoopGroup-2-1] ERROR 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:363)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:123)
io.netty.buffer.AbstractByteBuf.readBytes(AbstractByteBuf.java:872)
com.spring.netty.twg.service.TwgMessageDecoder.formatDecoder(TwgMessageDecoder.java:176)
com.spring.netty.twg.service.TwgMessageDecoder.getMessageBody(TwgMessageDecoder.java:90)
com.spring.netty.twg.service.TwgMessageDecoder.decode(TwgMessageDecoder.java:76)
io.netty.handler.codec.LengthFieldBasedFrameDecoder.decode(LengthFieldBasedFrameDecoder.java:332)
io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:501)


2020-06-12 17:04:45.460 [nioEventLoopGroup-2-1] ERROR 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.SimpleLeakAwareByteBuf.unwrappedDerived(SimpleLeakAwareByteBuf.java:143)
io.netty.buffer.SimpleLeakAwareByteBuf.retainedSlice(SimpleLeakAwareByteBuf.java:57)
io.netty.handler.codec.LengthFieldBasedFrameDecoder.extractFrame(LengthFieldBasedFrameDecoder.java:498)
io.netty.handler.codec.LengthFieldBasedFrameDecoder.decode(LengthFieldBasedFrameDecoder.java:437)
com.spring.netty.twg.service.TwgMessageDecoder.decode(TwgMessageDecoder.java:31)
io.netty.handler.codec.LengthFieldBasedFrameDecoder.decode(LengthFieldBasedFrameDecoder.java:332)

参考资料
https://netty.io/wiki/reference-counted-objects.html#who-destroys-it
https://blog.csdn.net/u013202238/article/details/93383887
Netty中ByteBuf内存泄露及释放解析的更多相关文章
- Netty如何监控内存泄露
目录 Netty如何监控内存泄露 前言 JDK的弱引用和引用队列 Netty的实现思路 代码实现 分配监控对象 追踪和检查泄露 Netty如何监控内存泄露 前言 一般而言,在Netty程序中都会采用池 ...
- Netty 中的内存分配浅析-数据容器
本篇接续前一篇继续讲 Netty 中的内存分配.上一篇 先简单做一下回顾: Netty 为了更高效的管理内存,自己实现了一套内存管理的逻辑,借鉴 jemalloc 的思想实现了一套池化内存管理的思路: ...
- Netty 中的内存分配浅析
Netty 出发点作为一款高性能的 RPC 框架必然涉及到频繁的内存分配销毁操作,如果是在堆上分配内存空间将会触发频繁的GC,JDK 在1.4之后提供的 NIO 也已经提供了直接直接分配堆外内存空间的 ...
- 查找并修复Android中的内存泄露—OutOfMemoryError
[编者按]本文作者为来自南非约翰内斯堡的女程序员 Rebecca Franks,Rebecca 热衷于安卓开发,拥有4年安卓应用开发经验.有点完美主义者,喜爱美食. 本文系国内ITOM管理平台 One ...
- 利用Instrument Leak来发现App中的内存泄露
XCode提供了一组用于检测内存,调试动画,布局等的工具.对于调试一些性能问题,内存问题非常方便.这里我们使用Leak来发现代码中的内存泄露. 在Leak中启动我们的应用开始监控: 注意,在监控的时候 ...
- Java中的内存泄露 和 JVM GC(垃圾回收机制)
一.什么是Java中的内存泄露? 在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点, 首先,这些对象是可达的,即在有向图中,存在通路可以与其相连:其次,这些对象是无用的,即程序以 ...
- C语言中的内存分配与释放
C语言中的内存分配与释放 对C语言一直都是抱着学习的态度,很多都不懂,今天突然被问道C语言的内存分配问题,说了一些自己知道的,但感觉回答的并不完善,所以才有这篇笔记,总结一下C语言中内存分配的主要内容 ...
- Netty中ByteBuf的引用计数线程安全的实现原理
原文链接 Netty中ByteBuf的引用计数线程安全的实现原理 代码仓库地址 ByteBuf 实现了ReferenceCounted 接口,实现了引用计数接口,该接口的retain(int) 方法为 ...
- Android内存优化8 内存检测工具2 LeakCanary——直白的展现Android中的内存泄露
之前碰到的OOM问题,终于很直白的呈现在我的眼前:我尝试了MAT,但是发现不怎么会用.直到今天终于发现了这个新工具: 当我们的App中存在内存泄露时会在通知栏弹出通知: 当点击该通知时,会跳转到具体的 ...
- LeakCanary——直白的展现Android中的内存泄露
之前碰到的OOM问题,终于很直白的呈现在我的眼前:我尝试了MAT,但是发现不怎么会用.直到今天终于发现了这个新工具: 当我们的App中存在内存泄露时会在通知栏弹出通知: 当点击该通知时,会跳转到具体的 ...
随机推荐
- 2023年1月中国数据库排行榜:OceanBase 持续两月登顶,前四甲青云直上开新局
一元复始,万象更新. 国产数据库在经历过耕获菑畲的一年后,产品.生态.人才队伍建设等都取得了重大的进展.2023年1月 墨天轮中国数据库流行度排行 火热出炉,本月排行榜"属性"列新 ...
- threejs渲染基础的3D场景
// 创建一个场景对象 let scene = new THREE.Scene(); // 创建一个相机对象 let camera = new THREE.PerspectiveCamera(75, ...
- C++中左值和右值的区别
#include <iostream> using namespace std; // 什么是左值和右值 const 可以修饰左值和右值z // 左值:有地址的变量 // 右值:没有地址的 ...
- 【转】sqlplus/RMAN/lsnrctl 等工具连接缓慢
AIX上sqlplus /as sysdba rman target / 或者lsnrctl start时或者通过sqlplus system/oracle@orcl这样通过监听连接等方式来登陆时非常 ...
- jmeter-模拟带参数的请求与上传文件
请求参数照着前端请求的样式填即可: header带content: header不带content_type: 总结:上传文件时headers里面不能带content_type参数
- 实现.NET 4.0下的Task类相似功能组件
实现 .NET 4.0 下的 Task 类相似功能:TaskExCum 组件详解 引言 随着 .NET 技术的发展,异步编程模型逐渐成为现代应用程序开发中的标准实践之一..NET 4.5 引入了 Ta ...
- PHP word PDF excel 文档互转 预览 (linux libreoffice)
1--卸载系统当前的libreoffice 执行如下命令:yum remove libreoffice-* 2--为了能使用镜像,在etc 目录之下的hosts文件内加入代理. 注意没有加的话,可能 ...
- EVE-NG全面教程带下载资源以及链接-原创
有关于EVE-NG的教程和资源 https://forum.eve-ng.cn/forum.php 1. 经验丰富的专业同行较多 2. 官方论坛,权威可靠 3. 有免费且可靠的镜像可以下载 并且简单好 ...
- 佛祖保佑永无 BUG 代码注释
// // _oo0oo_ // o8888888o // 88" . "88 // (| -_- |) // 0\ = /0 // ___/`---'\___ // .' \\| ...
- 第二篇:低功耗模组Air724UG硬件设计手册
接着上篇,继续分享. 3.5 串口 模块提供了五个通用异步收发器:主串口 UART1.校准串口 UART2.通用串口 UART3.调试串口 HOST UART 和 ZSP UART. 3.5.1 ...