为了提升消息接收和发送性能,Netty针对ByteBuf的申请和释放采用池化技术,通过PooledByteBufAllocator可以创建基于内存池分配的ByteBuf对象,这样就避免了每次消息读写都申请和释放ByteBuf。由于ByteBuf涉及byte[]数组的创建和销毁,对于性能要求苛刻的系统而言,重用ByteBuf带来的性能收益是非常可观的。

内存池是一把双刃剑,如果使用不当,很容易带来内存泄漏和内存非法引用等问题,另外,除了内存池,Netty同时也支持非池化的ByteBuf,多种类型的ByteBuf功能存在一些差异,使用不当很容易带来各种问题。

业务路由分发模块使用Netty作为通信框架,负责协议消息的接入和路由转发,在功能测试时没有发现问题,转性能测试之后,运行一段时间就发现内存分配异常,服务端无法接收请求消息,系统吞吐量降为0。

1 路由转发服务代码

作为案例示例,对业务服务路由转发代码进行简化,以方便分析:

  1.  
    public class RouterServerHandler extends ChannelInboundHandlerAdapter {
  2.  
    static ExecutorService executorService = Executors.newSingleThreadExecutor();
  3.  
    PooledByteBufAllocator allocator = new PooledByteBufAllocator(false);
  4.  
    @Override
  5.  
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
  6.  
    ByteBuf reqMsg = (ByteBuf)msg;
  7.  
    byte [] body = new byte[reqMsg.readableBytes()];
  8.  
    executorService.execute(()->{
  9.  
    //解析请求消息,做路由转发,代码省略
  10.  
    //转发成功,返回响应给客户端
  11.  
    ByteBuf respMsg = allocator.heapBuffer(body.length);
  12.  
    respMsg.writeBytes(body);//作为示例,简化处理,将请求返回
  13.  
    ctx.writeAndFlush(respMsg);
  14.  
    });
  15.  
    }
  16.  
    }

进行一段时间的性能测试之后,日志中出现异常,进程内存不断飙升,怀疑存在内存泄漏问题,如下图所示。

2 响应消息内存释放玄机

对业务ByteBuf申请相关代码进行排查,发现响应消息由业务线程创建,但是却没有主动释放,因此怀疑是响应消息没有释放导致的内存泄漏。因为响应消息使用的是PooledHeapByteBuf,如果发生内存泄漏,利用堆内存监控就可以找到泄漏点,通过Java VisualVM工具观察堆内存占用趋势,并没有发现堆内存发生泄漏,如下图所示。

对内存做快照,查看在性能压测过程中响应消息PooledUnsafeHeapByteBuf的实例个数,如下图所示,响应消息对象个数和内存占用都很少,排除内存泄漏嫌疑。

业务从内存池中申请了ByteBuf,但是却没有主动释放它,最后也没有发生内存泄漏,这究竟是什么原因呢?通过对Netty源码的分析,我们破解了其中的玄机。原来调用ctx.writeAndFlush(respMsg)方法时,当消息发送完成,Netty框架会主动帮助应用释放内存,内存的释放分为如下两种场景。

(1)如果是堆内存(PooledHeapByteBuf),则将HeapByteBuffer转换成DirectByteBuffer,并释放PooledHeapByteBuf到内存池,代码如下(AbstractNioChannel类):

  1.  
    protected final ByteBuf newDirectBuffer(ByteBuf buf) {
  2.  
    final int readableBytes = buf.readableBytes();
  3.  
    if (readableBytes == 0) {
  4.  
    ReferenceCountUtil.safeRelease(buf);
  5.  
    return Unpooled.EMPTY_BUFFER;
  6.  
    }
  7.  
    final ByteBufAllocator alloc = alloc();
  8.  
    if (alloc.isDirectBufferPooled()) {
  9.  
    ByteBuf directBuf = alloc.directBuffer(readableBytes);
  10.  
    directBuf.writeBytes(buf, buf.readerIndex(), readableBytes);
  11.  
    ReferenceCountUtil.safeRelease(buf);
  12.  
    return directBuf;
  13.  
    }
  14.  
    }

如果消息完整地被写到SocketChannel中,则释放DirectByteBuffer,代码如下(ChannelOutboundBuffer):

  1.  
    public boolean remove() {
  2.  
    Entry e = flushedEntry;
  3.  
    if (e == null) {
  4.  
    clearNioBuffers();
  5.  
    return false;
  6.  
    }
  7.  
    Object msg = e.msg;
  8.  
    ChannelPromise promise = e.promise;
  9.  
    int size = e.pendingSize;
  10.  
    removeEntry(e);
  11.  
    if (!e.cancelled) {
  12.  
    ReferenceCountUtil.safeRelease(msg);
  13.  
    safeSuccess(promise);
  14.  
    decrementPendingOutboundBytes(size, false, true);
  15.  
    }
  16.  
    }

对Netty源码进行断点调试,验证上述分析。

断点1:在响应消息发送处设置断点,获取到的PooledUnsafeHeapByteBuf实例的ID为1506,如下图所示。

断点2:在HeapByteBuffer转换成DirectByteBuffer处设置断点,发现实例ID为1506的PooledUnsafeHeapByteBuf被释放,如下图所示。

断点3:转换之后待发送的响应消息PooledUnsafeDirectByteBuf实例的ID为1527,如下图所示。

断点4:在响应消息发送完成后,实例ID为1527的PooledUnsafeDirectByteBuf被释放到内存池中,如下图所示。

(2)如果是DirectByteBuffer,则不需要转换,在消息发送完成后,由ChannelOutboundBuffer的remove()负责释放。

通过源码解读、调试及堆内存的监控分析,可以确认不是响应消息没有主动释放导致的内存泄漏,需要Dump内存做进一步定位。

3 采集堆内存快照分析

执行jmap命令,Dump应用内存堆栈,如图8所示。

通过MemoryAnalyzer工具对内存堆栈进行分析,寻找内存泄漏点,如图9所示。

从下图可以看出,内存泄漏点是Netty内存池对象PoolChunk,由于请求和响应消息内存分配都来自PoolChunk,暂时还不确认是请求还是响应消息导致的问题。进一步对代码进行分析,发现响应消息使用的是堆内存HeapByteBuffer,请求消息使用的是DirectByteBuffer,由于Dump出来的是堆内存,如果是堆内存泄漏,Dump出来的内存文件应该包含大量的PooledHeapByteBuf,实际上并没有,因此可以确认系统发生了堆外内存泄漏,即请求消息没有被释放或者没有被及时释放导致的内存泄漏。

对请求消息的内存分配进行分析,发现在NioByteUnsafe的read方法中申请了内存,代码如下(NioByteUnsafe):

  1.  
    public final void read() {
  2.  
    final ChannelConfig config = config();
  3.  
    if (shouldBreakReadReady(config)) {
  4.  
    clearReadPending();
  5.  
    return;
  6.  
    }
  7.  
    final ChannelPipeline pipeline = pipeline();
  8.  
    final ByteBufAllocator allocator = config.getAllocator();
  9.  
    final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
  10.  
    allocHandle.reset(config);
  11.  
    ByteBuf byteBuf = null;
  12.  
    boolean close = false;
  13.  
    //代码省略

继续对allocate方法进行分析,发现调用的是DefaultMaxMessagesRecvByteBuf- Allocator$MaxMessageHandle的allocate方法,代码如下(DefaultMaxMessagesRecvByteBuf- Allocator):

  1.  
    public ByteBuf allocate(ByteBufAllocator alloc) {
  2.  
    return alloc.ioBuffer(guess());
  3.  
    }

alloc.ioBuffer方法最终会调用PooledByteBufAllocator的newDirectBuffer方法创建PooledDirectByteBuf对象。

请求ByteBuf的创建分析完,继续分析它的释放操作,由于业务的RouterServerHandler继承自ChannelInboundHandlerAdapter,它的channelRead(ChannelHandlerContext ctx, Object msg)方法执行完成,ChannelHandler的执行就结束了,代码示例如下:

  1.  
    @Override
  2.  
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
  3.  
    ByteBuf reqMsg = (ByteBuf)msg;
  4.  
    byte [] body = new byte[reqMsg.readableBytes()];
  5.  
    executorService.execute(()-> {
  6.  
    //解析请求消息,做路由转发,代码省略
  7.  
    //转发成功,返回响应给客户端
  8.  
    ByteBuf respMsg = allocator.heapBuffer(body.length);
  9.  
    respMsg.writeBytes(body);//作为示例,简化处理,将请求返回
  10.  
    ctx.writeAndFlush(respMsg);
  11.  
    });
  12.  
    }

通过代码分析发现,请求ByteBuf被Netty框架申请后竟然没有被释放,为了验证分析,在业务代码中调用ReferenceCountUtil的release方法进行内存释放操作,代码修改如下:

  1.  
    @Override
  2.  
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
  3.  
    ByteBuf reqMsg = (ByteBuf)msg;byte [] body = new byte[reqMsg.readableBytes()];
  4.  
    ReferenceCountUtil.release(reqMsg);
  5.  
    //后续代码省略

修改之后继续进行压测,发现系统运行平稳,没有发生OOM异常。对内存活动对象进行排序,没有再发现大量的PoolChunk对象,内存泄漏问题解决,问题修复之后的内存快照如下图所示。

4 ByteBuf申请和释放的理解误区

有一种说法认为Netty框架分配的ByteBuf框架会自动释放,业务不需要释放;业务创建的ByteBuf则需要自己释放,Netty框架不会释放。

通过前面的案例分析和验证,我们可以看出这个观点是错误的。为了在实际项目中更好地管理ByteBuf,下面我们分4种场景进行说明。

1.基于内存池的请求ByteBuf

这类ByteBuf主要包括PooledDirectByteBuf和PooledHeapByteBuf,它由Netty的NioEventLoop线程在处理Channel的读操作时分配,需要在业务ChannelInboundHandler处理完请求消息之后释放(通常在解码之后),它的释放有两种策略。

策略1 业务ChannelInboundHandler继承自SimpleChannelInboundHandler,实现它的抽象方法channelRead0(ChannelHandlerContext ctx, I msg),ByteBuf的释放业务不用关心,由SimpleChannelInboundHandler负责释放,相关代码如下(SimpleChannelInboundHandler):

  1.  
    @Override
  2.  
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  3.  
    boolean release = true;
  4.  
    try {
  5.  
    if (acceptInboundMessage(msg)) {
  6.  
    I imsg = (I) msg;
  7.  
    channelRead0(ctx, imsg);
  8.  
    } else {
  9.  
    release = false;
  10.  
    ctx.fireChannelRead(msg);
  11.  
    }
  12.  
    } finally {
  13.  
    if (autoRelease && re lease) {
  14.  
    ReferenceCountUtil.release(msg);
  15.  
    }
  16.  
    }
  17.  
    }

如果当前业务ChannelInboundHandler需要执行,则调用channelRead0之后执行ReferenceCountUtil.release(msg)释放当前请求消息。如果没有匹配上需要继续执行后续的ChannelInboundHandler,则不释放当前请求消息,调用ctx.fireChannelRead(msg)驱动ChannelPipeline继续执行。

对案例中的问题代码进行修改,继承自SimpleChannelInboundHandler,即便业务不释放请求的ByteBuf对象,依然不会发生内存泄漏,修改之后的代码如下(RouterServerHandlerV2):

  1.  
    public class RouterServerHandlerV2 extends SimpleChannelInboundHandler <ByteBuf> {
  2.  
     
  3.  
    @Override
  4.  
    public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
  5.  
    byte [] body = new byte[msg.readableBytes()];
  6.  
    executorService.execute(()-> {
  7.  
    //解析请求消息,做路由转发,代码省略
  8.  
    //转发成功,返回响应给客户端
  9.  
    ByteBuf respMsg = allocator.heapBuffer(body.length);
  10.  
    respMsg.writeBytes(body);//作为示例,简化处理,将请求返回
  11.  
    ctx.writeAndFlush(respMsg);
  12.  
    });
  13.  
    }

对修改之后的代码做性能测试,发现内存占用平稳,无内存泄漏问题,验证了之前的分析结论。

策略2 在业务ChannelInboundHandler中调用ctx.fireChannelRead(msg)方法,让请求消息继续向后执行,直到调用DefaultChannelPipeline的内部类TailContext,由它来负责释放请求消息,代码如下(TailContext):

  1.  
    protected void onUnhandledInboundMessage(Object msg) {
  2.  
    try {
  3.  
    logger.debug( "Discarded inbound message {} that reached at the tail of thpipeline." +
  4.  
    "Please check your pipeline configuration.", msg);
  5.  
    } finally {
  6.  
    ReferenceCountUtil.release(msg);
  7.  
    }
  8.  
    }

2.基于非内存池的请求ByteBuf

如果业务使用非内存池模式覆盖Netty默认的内存池模式创建请求ByteBuf,例如通过如下代码修改内存申请策略为Unpooled:

  1.  
    //代码省略
  2.  
    childHandler(new ChannelInitializer<SocketChannel>() {
  3.  
    @Override
  4.  
    public void initChannel(SocketChannel ch) throws Exception {
  5.  
    ChannelPipeline p = ch.pipeline();
  6.  
    ch.config().setAllocator(UnpooledByteBufAllocator.DEFAULT);
  7.  
    p.addLast(new RouterServerHandler());
  8.  
    }
  9.  
    }

也需要按照内存池的方式释放内存。

3.基于内存池的响应ByteBuf

根据之前的分析,只要调用了writeAndFlush或者flush方法,在消息发送完成后都会由Netty框架进行内存释放,业务不需要主动释放内存。

4.基于非内存池的响应ByteBuf

无论是基于内存池还是非内存池分配的ByteBuf,如果是堆内存,则将堆内存转换成堆外内存,然后释放HeapByteBuffer,待消息发送完成,再释放转换后的DirectByteBuf;如果是DirectByteBuffer,则不需要转换,待消息发送完成之后释放。因此对于需要发送的响应ByteBuf,由业务创建,但是不需要由业务来释放

Netty内存池泄漏问题的更多相关文章

  1. Netty内存池及命中缓存的分配

    内存池的内存规格: 在前面的源码分析过程中,关于内存规格大小我们应该还有些印象.其实在Netty 内存池中主要设置了四种规格大小的内存:tiny 是指0-512Byte 之间的规格大小,small 是 ...

  2. Netty内存池的整体架构

    一.为什么要实现内存管理? Netty 作为底层网络通信框架,网络IO读写必定是非常频繁的操作,考虑到更高效的网络传输性能,堆外内存DirectByteBuffer必然是最合适的选择.堆外内存在 JV ...

  3. Netty内存池ByteBuf 内存回收

    内存池ByteBuf 内存回收: 在前面的章节中我们有提到, 堆外内存是不受JVM 垃圾回收机制控制的, 所以我们分配一块堆外内存进行ByteBuf 操作时, 使用完毕要对对象进行回收, 本节就以Po ...

  4. Netty内存池

    参考资料:http://blog.csdn.net/youaremoon/article/details/47910971 主要思想:buddy allocation,jemalloc

  5. 感悟优化——Netty对JDK缓冲区的内存池零拷贝改造

    NIO中缓冲区是数据传输的基础,JDK通过ByteBuffer实现,Netty框架中并未采用JDK原生的ByteBuffer,而是构造了ByteBuf. ByteBuf对ByteBuffer做了大量的 ...

  6. PooledByteBuf内存池-------这个我现在不太懂

    转载自:http://blog.csdn.net/youaremoon/article/details/47910971              http://blog.csdn.net/youar ...

  7. Netty精粹之轻量级内存池技术实现原理与应用

    摘要: 在Netty中,通常会有多个IO线程独立工作,基于NioEventLoop的实现,每个IO线程负责轮询单独的Selector实例来检索IO事件,当IO事件来临的时候,IO线程开始处理IO事件. ...

  8. netty源码解解析(4.0)-24 ByteBuf基于内存池的内存管理

    io.netty.buffer.PooledByteBuf<T>使用内存池中的一块内存作为自己的数据内存,这个块内存是PoolChunk<T>的一部分.PooledByteBu ...

  9. netty源码解析(4.0)-28 ByteBuf内存池:PooledByteBufAllocator-把一切组装起来

    PooledByteBufAllocator负责初始化PoolArena(PA)和PoolThreadCache(PTC).它提供了一系列的接口,用来创建使用堆内存或直接内存的PooledByteBu ...

  10. netty源码解析(4.0)-27 ByteBuf内存池:PoolArena-PoolThreadCache

    前面两章分析的PoolChunk和PoolSubpage,从功能上来说已经可以直接拿来用了.但直接使用这个两个类管理内存在高频分配/释放内存场景下会有性能问题,PoolChunk分配内存时算法复杂度最 ...

随机推荐

  1. 前端工程化解决方案webpack使用小结

    前端工程化解决方案webpack,模块化.组件化.规范化.自动化,使得前端开发更加高效. 功能:代码压缩混淆.处理浏览器端js的兼容性.以模块化的方式处理项目中的资源 webpack插件:clean- ...

  2. 后台管理系统的setting.js

    // 修改了此处要重新启动 module.exports = { // 网页的标题 title: "人力资源系统", /** * @type {boolean} true | fa ...

  3. Java和Python的区别

    Java和Python区别 二者的区别有以下几点:1.Java必须显式声明变量名,而动态类型的Python不需要声明变量.2.Python虚拟机没有Java强,Java虚拟机是Java的核心,Pyth ...

  4. 深度学习系列之1----直观解释Transformer

    Abstract 这个系列主要用来记录我自己这种的AI小白的学习之路,通过将所学所知总结下来,记录下来.之前总喜欢记录在笔记本上,或者ipad上,或者PC端的Typora上,但总是很难回头检索到一些系 ...

  5. 使用wxpython开发跨平台桌面应用,基类对话框窗体的封装处理

    在开发桌面界面的时候,往往都需要对一些通用的窗体进行一些抽象封装处理,以便统一界面效果,以及继承一些通用的处理过程,减少重复编码.本篇随笔介绍使用wxpython开发跨平台桌面应用,基类对话框窗体的封 ...

  6. 3.1 Linux文件系统的层次结构

    通过学习<Linux一切皆文件>一节我们知道,平时打交道的都是文件,那么,应该如何找到它们呢?很简单,在 Linux 操作系统中,所有的文件和目录都被组织成以一个根节点"/&qu ...

  7. 2024-11-20:交替子数组计数。用go语言,给定一个二进制数组 nums, 如果一个子数组中的相邻元素的值都不相同,我们称这个子数组为交替子数组。 请返回数组 nums 中交替子数组的总数。 输

    2024-11-20:交替子数组计数.用go语言,给定一个二进制数组 nums, 如果一个子数组中的相邻元素的值都不相同,我们称这个子数组为交替子数组. 请返回数组 nums 中交替子数组的总数. 输 ...

  8. QT6.8 编译 MSVC2022-64位MySQL驱动

    QT6.8没有编译MySql驱动,也没有.pro的项目文件,只能自己想办法编译,网上找了很多方法,终于找到了可以成功编译的方法,下面将我的编译过程详细记录如下: [声明:本文为原创,未经允许,不得转载 ...

  9. nemu-wsl-环境配置

    实在是不愿意用学校的虚拟平台,觉得在自己的电脑上留存一部分真的很有意思,也想捣鼓一下,于是在自己电脑上配置下最基本的环境,做下记录 准备好wsl 因为要求环境是 Ubuntu 18.04 和 gcc- ...

  10. 数据库研发人员必看的MySQL 8.0新特性

    本文汇总了MySQL8.0 面向开发的新特性,总共有12个新特性,有想快速了解8.0新特性的朋友,可以看一下哈文章目录:1.公用表达式支持-CTE2.窗口函数3.表达式作为默认值:4.CHECK支持5 ...