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中存在内存泄露时会在通知栏弹出通知: 当点击该通知时,会跳转到具体的 ...
随机推荐
- Solon 3.0 新特性:SqlUtils
Solon 3.0 引入了新的 SqlUtils 用于数据库基础操作,SqlUtils 是对 JDBC 较为原始的封装,采用了 Utils API 的风格,极为反普归真. 特性有: 支持事务管理 支持 ...
- 通过脚手架 npx 创建 react 项目
npx create-react-app create-react --template typescript 使用 ts 模板
- Vue 组件如何进行传值的?
1 父子传值 在子组件标签设置属性,在子组件内使用 props 接收属性值 : 2. 子父传值 在子组件中使用 emit 自定义事件,在子组件标签注册自定义事件 ,接收参数 : 3. vuex 状态管 ...
- Kubernetes的RBAC权限控制
role和roleBinding Role资源定义了哪些操作可以在哪些资源上执行.也可以直接控制访问的url的权限,下面的cluster也是这样. 查询所有service的demo: apiVersi ...
- KubeSphere 社区双周报|2024.03.15-03.29
KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书.新增的讲师证书以及两周内提交过 commit 的贡献者,并对近期重要的 PR 进行解析,同时还包含了线上/线下活动和布道推广等一系列 ...
- C++ mutable与常对象语义详解
摘编自 <Effective C++> 条款三. "成员函数如果是const" 或者 "一个对象是const对象"到底意味什么?有两个流行概念:bi ...
- ToDesk云电脑游戏数量?高性能显卡云桌面
玩游戏最怕遇到电脑配置跟不上,操作卡成狗不说,画面还一卡卡的,游戏体验极差. 最近被人安利了ToDesk的云电脑,可能是刚推出的,配置价格都很能打,浅用了一波拿来打APEX和荒野大镖客,体验有点惊喜到 ...
- ABC372 (D,E)
ABC372 (D,E) D 一道比较简单的二分查找题目. 观察到每个数能成为 \(j\) 的条件是独立的,因此想到统计每个数能成为它前面哪些数的 \(j\). 对于每个\(ed\), 二分 \(1 ...
- 2024SHCTF--Crypto--Week1&Week2--WP
2024SHCTF 注:针对2024SHCTF赛事,写下自己的解题思路以及个别赛题赛后复现对于题目而产生的理解. Week1 d_known task: from Crypto.Util.number ...
- Abp源码分析之虚拟文件系统Volo.Abp.VirtualFileSystem
前言 Volo.Abp.VirtualFileSystem 是ABP(ASP.NET Boilerplate)框架中的一个重要组件,它提供了一种抽象文件系统的方式,使得应用程序可以轻松地访问和管理文件 ...