Netty ByteBuf(图解 )之一

疯狂创客圈 Java 分布式聊天室【 亿级流量】实战系列之15 【 博客园 总入口 】


源码工程

源码IDEA工程获取链接Java 聊天室 实战 源码

写在前面

​ 大家好,我是作者尼恩。

​ 今天是百万级流量 Netty 聊天器 打造的系列文章的第15篇,这是一个基础篇。

​ 由于关于ByteBuf的内容比较多,分两篇文章:

​ 第一篇:图解 ByteBuf的分配、释放和如何避免内存泄露

​ 第二篇:图解 ByteBuf的具体使用

本篇为第一篇

Netty ByteBuf 优势

Netty 提供了ByteBuf,来替代Java NIO的 ByteBuffer 缓,来操纵内存缓冲区。

与Java NIO的 ByteBuffer 相比,ByteBuf的优势如下:

  1. Pooling (池化,这点减少了内存复制和GC,提升效率)

  2. 可以自定义缓冲类型

  3. 通过一个内置的复合缓冲类型实现零拷贝

  4. 扩展性好,比如 StringBuffer

  5. 不需要调用 flip()来切换读/写模式

  6. 读取和写入索引分开

  7. 方法链

  8. 引用计数

手动获取与释放ByteBuf

Netty环境下,业务处理的代码,基本上都在Handler处理器中的各个入站和出站方法中。

一般情况下,采用如下方法获取一个Java 堆中的缓冲区:

ByteBuf heapBuffer = ctx.alloc().heapBuffer();

使用完成后,通过如下的方法,释放缓冲区:

ReferenceCountUtil.release(heapBuffer );

上面的代码很简单,通过release方法减去 heapBuffer 的使用计数,Netty 会自动回收 heapBuffer 。

缓冲区内存的回收、二次分配等管理工作,是 Netty 自动完成的。

自动获取和释放 ByteBuf

方式一:TailHandler 自动释放

​ Netty默认会在ChannelPipline的最后添加的那个 TailHandler 帮你完成 ByteBuf的release。

​ 先看看,自动创建的ByteBuf实例是如何登场的?

Netty自动创建 ByteBuf实例

​ Netty 的 Reactor 线程会在 AbstractNioByteChannel.NioByteUnsafe.read() 处调用 ByteBufAllocator创建ByteBuf实例,将TCP缓冲区的数据读取到 Bytebuf 实例中,并调用 pipeline.fireChannelRead(byteBuf) 进入pipeline 入站处理流水线。

默认情况下,TailHandler自动释放掉ByteBuf实例

​ Netty的ChannelPipleline的流水线的末端是TailHandler,默认情况下如果每个入站处理器Handler都把消息往下传,TailHandler会释放掉ReferenceCounted类型的消息。

说明:

上图中,TailHandler 写成了TailContext,这个是没有错的。

对于流水线的头部和尾部Hander来说, Context和Hander ,是同一个类。

HeadContext 与HeadHandler ,也是同一个类。

关于Context与Handler 的关系,请看 疯狂创客圈 的系列文章。

如果没有到达末端呢?

​ 一种没有到达入站处理流水线pipeline末端的情况,如下图所示:

​ 这种场景下,也有一种自动释放的解决办法,它就是:

​ 可以继承 SimpleChannelInboundHandler,实现业务Handler。 SimpleChannelInboundHandler 会完成ByteBuf 的自动释放,释放的处理工作,在其入站处理方法 channelRead 中。

方式二:SimpleChannelInboundHandler 自动释放

​ 如果业务Handler需要将 ChannelPipleline的流水线的默认处理流程截断,不进行后边的inbound入站处理操作,这时候末端 TailHandler自动释放缓冲区的工作,自然就失效了。

​ 这种场景下,业务Handler 有两种选择:

  • 手动释放 ByteBuf 实例

  • 继承 SimpleChannelInboundHandler,利用它的自动释放功能。

本小节,我们聚焦的是第二种选择:看看 SimpleChannelInboundHandler是如何自动释放的。

利用这种方法,业务处理Handler 必须继承 SimpleChannelInboundHandler基类。并且,业务处理的代码,必须 移动到 重写的 channelRead0(ctx, msg)方法中。

如果好奇,想看看 SimpleChannelInboundHandler 是如何释放ByteBuf 的,那就一起来看看Netty源码。

截取的代码如下所示:

  1. public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter
  2. {
  3. //...
  4. @Override
  5. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  6. boolean release = true;
  7. try {
  8. if (acceptInboundMessage(msg)) {
  9. @SuppressWarnings("unchecked")
  10. I imsg = (I) msg;
  11. channelRead0(ctx, imsg);
  12. } else {
  13. release = false;
  14. ctx.fireChannelRead(msg);
  15. }
  16. } finally {
  17. if (autoRelease && release) {
  18. ReferenceCountUtil.release(msg);
  19. }
  20. }
  21. }

源码中,执行完重写的channelRead0()后,在 finally 语句块中,ByteBuf 的生命被结束掉了。

上面两种,都是入站处理(inbound)过程中的自动释放。

出站处理(outbound)流程,又是如何自动释放呢?

方式三:HeadHandler 自动释放

出站处理流程中,申请分配到的 ByteBuf,通过 HeadHandler 完成自动释放。

出站处理用到的 Bytebuf 缓冲区,一般是要发送的消息,通常由应用所申请。在出站流程开始的时候,通过调用 ctx.writeAndFlush(msg),Bytebuf 缓冲区开始进入出站处理的 pipeline 流水线 。在每一个出站Handler中的处理完成后,最后消息会来到出站的最后一棒 HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。

强调一下,HeadContext (HeadHandler)是出站处理流程的最后一棒

出站处理的全过程,请查看疯狂创客圈的专门文章

如何避免内存泄露

​ 基本上,在 Netty的开发中,通过 ChannelHandlerContext 或 Channel 获取的缓冲区ByteBuf 默认都是Pooled,所以需要再合适的时机对其进行释放,避免造成内存泄漏。

自动释放的注意事项

我们已经知道了三种自动释放方法:

  • 通过 TailHandler 自动释放入站 ByteBuf

  • 继承 SimpleChannelInboundHandler 的完成 入站ByteBuf 自动释放

  • 通过HeadHandler自动释放出站 ByteBuf

    自动释放,注意事项如下

  • 入站处理流程中,如果对原消息不做处理,默认会调用 ctx.fireChannelRead(msg) 把原消息往下传,由流水线最后一棒 TailHandler 完成自动释放。

  • 如果截断了入站处理流水线,则可以继承 SimpleChannelInboundHandler ,完成入站ByteBuf 自动释放。

  • 出站处理过程中,申请分配到的 ByteBuf,通过 HeadHandler 完成自动释放。

    出站处理用到的 Bytebuf 缓冲区,一般是要发送的消息,通常由应用所申请。在出站流程开始的时候,通过调用 ctx.writeAndFlush(msg),Bytebuf 缓冲区开始进入出站处理的 pipeline 流水线 。在每一个出站Handler中的处理完成后,最后消息会来到出站的最后一棒 HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。

手动释放的注意事项

​ 手动释放是自动释放的重要补充和辅助。

手动释放操作,大致有如下注意事项

  • 入站处理中,如果将原消息转化为新的消息并调用 ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉;

  • 入站处理中,如果已经不再调用 ctx.fireChannelRead(msg) 传递任何消息,也没有继承SimpleChannelInboundHandler 完成自动释放,那更要把原消息release掉;

  • 多层的异常处理机制,有些异常处理的地方不一定准确知道ByteBuf之前释放了没有,可以在释放前加上引用计数大于0的判断避免异常; 有时候不清楚ByteBuf被引用了多少次,但又必须在此进行彻底的释放,可以循环调用reelase()直到返回true。

特别需要强调的,是上边的第一种情况

如果在入站处理的 handlers 传递过程中,传递了新的ByteBuf 值,老ByteBuf 值需要自己手动释放。老的ByteBuf 值,就是从pipeline流水线入口传递过来的 ByteBuf 实例。

总之,只要是在传递过程中,没有传递下去的ByteBuf就需要手动释放,避免不必要的内存泄露

缓冲区 Allocator 分配器

Netty通过 ByteBufAllocator分配缓冲区。

Netty提供了ByteBufAllocator的两种实现:PoolByteBufAllocator和UnpooledByteBufAllocator。前者将ByteBuf实例放入池中,提高了性能,将内存碎片减少到最小。这个实现采用了一种内存分配的高效策略,称为 jemalloc。它已经被好几种现代操作系统所采用。后者则没有把ByteBuf放入池中,每次被调用时,返回一个新的ByteBuf实例。

分配器 Allocator的类型

PooledByteBufAllocator:可以重复利用之前分配的内存空间。

为了减少内存的分配回收以及产生的内存碎片,Netty提供了PooledByteBufAllocator 用来分配可回收的ByteBuf,可以把PooledByteBufAllocator 看做一个池子,需要的时候从里面获取ByteBuf,用完了放回去,以此提高性能。

UnpooledByteBufAllocator:不可重复利用,由JVM GC负责回收

顾名思义Unpooled就是不会放到池子里,所以根据该分配器分配的ByteBuf,不需要放回池子,由JVM自己GC回收。

这两个类,都是AbstractByteBufAllocator的子类,AbstractByteBufAllocator实现了一个接口,叫做ByteBufAllocator。

可以做一个对比试验

使用UnpooledByteBufAllocator的方式创建ByteBuf的时候,单台24核CPU的服务器,16G内存,刚启动时候,10000个长连接,每秒所有的连接发一条消息,短时间内,可以看到内存占到10G多点,但随着系统的运行,内存不断增长,直到整个系统内存溢出挂掉。

把UnpooledByteBufAllocator换成PooledByteBufAllocator,通过试验,内存使用量机器能维持在一个连接占用1M左右,内存在10G左右,经常长期的运行测试,发现都能维持在这个数量,系统内存不会崩溃。

默认的分配器

​ 默认的分配器 ByteBufAllocator.DEFAULT ,可以通过 Java 系统参数(SystemProperty )选项 io.netty.allocator.type 去配置,使用字符串值:"unpooled","pooled"。

​ 关于这一段,Netty的源代码截取如下:

  1. String allocType = SystemPropertyUtil.get("io.netty.allocator.type", "unpooled").toLowerCase(Locale.US).trim();
  2. Object alloc;
  3. if("unpooled".equals(allocType)) {
  4. alloc = UnpooledByteBufAllocator.DEFAULT;
  5. logger.debug("-Dio.netty.allocator.type: {}", allocType);
  6. } else if("pooled".equals(allocType)) {
  7. alloc = PooledByteBufAllocator.DEFAULT;
  8. logger.debug("-Dio.netty.allocator.type: {}", allocType);
  9. } else {
  10. alloc = UnpooledByteBufAllocator.DEFAULT;
  11. logger.debug("-Dio.netty.allocator.type: unpooled (unknown: {})", allocType);
  12. }
  1. 不同的Netty版本,源码不一样。

​ 上面的代码,是4.0版本的源码,默认为UnpooledByteBufAllocator。

​ 而4.1 版本,默认为 PooledByteBufAllocator。因此,4.1版本的代码,是和上面的代码稍微有些不同的。

设置通道Channel的分配器

在4.x版本中,UnpooledByteBufAllocator是默认的allocator,尽管其存在某些限制。

现在PooledByteBufAllocator已经广泛使用一段时间,并且我们有了增强的缓冲区泄漏追踪机制,所以是时候让PooledByteBufAllocator成为默认了。

  1. ServerBootstrap b = new ServerBootstrap()
  2. .group(bossGroup, workerGroup)
  3. .channel(NioServerSocketChannel.class)
  4. .localAddress(port)
  5. .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
  6. .childHandler(new ChannelInitializer<SocketChannel>() {
  7. @Override
  8. public void initChannel(SocketChannel ch) throws Exception {
  9. ch.pipeline().addLast(...);
  10. }
  11. });

使用Netty带来的又一个好处就是内存管理。只需一行简单的配置,就能获得到内存池带来的好处。在底层,Netty实现了一个Java版的Jemalloc内存管理库,为我们做完了所有“脏活累活”!

缓冲区内存的类型

​ 说完了分配器的类型,再来说下缓冲区的类型。

​ 依据内存的管理方不同,分为堆缓存和直接缓存。也就是Heap ByteBuf 和 Direct ByteBuf。另外,为了方便缓冲区进行组合,提供了一种组合缓存区。



​ 三种缓冲区的介绍如下:

使用模式 描述 优点 劣势
堆缓冲区 数据存存储在JVM的堆空间中,又称为支撑数组,通过 hasArray 来判断是不是在堆缓冲区中 没使用池化情况下能提供快速的分配和释放 发送之前都会拷贝到直接缓冲区
直接缓冲区 存储在物理内存中 能获取超过jvm堆限制大小的空间;
写入channel比堆缓冲区更快
释放和分配空间昂贵(使用系统的方法) ;
操作时需要复制一次到堆上
复合缓冲 单个缓冲区合并多个缓冲区表示 操作多个更方便 -

​ 上面三种缓冲区的类型,无论哪一种,都可以通过池化、非池化的方式,去获取。

Unpooled 非池化缓冲区的使用方法

Unpooled也是用来创建缓冲区的工具类,Unpooled 的使用也很容易。

看下面代码:

//创建复合缓冲区

CompositeByteBuf compBuf = Unpooled.compositeBuffer();

//创建堆缓冲区

ByteBuf heapBuf = Unpooled.buffer(8);

//创建直接缓冲区

ByteBuf directBuf = Unpooled.directBuffer(16);

Unpooled 提供了很多方法,详细方法大致如下:

方法名称 描述
buffer()
buffer(int initialCapacity)
buffer(int initialCapacity, int maxCapacity)
返回 heap ByteBuf
directBuffer()
directBuffer(int initialCapacity)
directBuffer(int initialCapacity, intmaxCapacity)
返回 direct ByteBuf
compositeBuffer() 返回 CompositeByteBuf
copiedBuffer() 返回 copied ByteBuf

Unpooled类的应用场景

Unpooled类让ByteBuf也同样适用于不需要其他的Netty组件的、无网络操作的项目,这些项目可以从这个高性能的、可扩展的buffer API中获益。

写在最后

​ 至此为止,终于完成ByteBuf的分配、释放和如何避免内存泄露介绍。

​ 接下来是:

​ 第二篇:图解 ByteBuf的具体使用


疯狂创客圈 Java 死磕系列

  • Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战

ByteBuf(图解1)的更多相关文章

  1. Netty ByteBuf(图解之 2)| 秒懂

    目录 Netty ByteBuf(图解二):API 图解 源码工程 写在前面 ByteBuf 的四个逻辑部分 ByteBuf 的三个指针 ByteBuf 的三组方法 ByteBuf 的引用计数 Byt ...

  2. Netty(一):ByteBuf读写过程图解

    我们知道ByteBuf通过读写两个索引分离,避免了NIO中ByteBuffer中读写模式切换时,需要flip等繁琐的操作. 今天就通过一段测试代码以及图例来直观的了解下ByteBuf中的readInd ...

  3. Redis分布式锁 (图解-秒懂-史上最全)

    文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...

  4. 图解CSS3制作圆环形进度条的实例教程

    圆环形进度条制作的基本思想还是画出基本的弧线图形,然后CSS3中我们可以控制其旋转来串联基本图形,制造出部分消失的效果,下面就来带大家学习图解CSS3制作圆环形进度条的实例教程 首先,当有人说你能不能 ...

  5. 《图解HTTP》读书笔记

    目前国内讲解HTTP协议的书是在太少了,记忆中有两本被誉为经典的书<HTTP权威指南>与<TCP/IP详解,卷1>,但内容晦涩难懂,学习难度较大.其实,HTTP协议并不复杂,理 ...

  6. [PostgreSQL] 图解安装 PostgreSQL

    图解安装 PostgreSQL [博主]反骨仔 [原文地址]http://www.cnblogs.com/liqingwen/p/5894462.html 序 园友的一篇<Asp.Net Cor ...

  7. 图解ios程序生命周期

    图解ios程序生命周期 应用程序启动后状态有Active.Inactive.Background.Suspended.Not running这5种状态,几种状态的转换见下图: 在AppDelegate ...

  8. 【用xocde5打包 在IOS7以下也能显示无默认gloss 效果 图解】

    图解,只需要这几个地方修改以下就好了,看不懂的话,请留言 完毕:记着clean 以下项目文件

  9. Python开发工具PyCharm个性化设置(图解)

    Python开发工具PyCharm个性化设置,包括设置默认PyCharm解析器.设置缩进符为制表符.设置IDE皮肤主题等,大家参考使用吧. JetBrains PyCharm Pro 4.5.3 中文 ...

随机推荐

  1. JavaScript 在双引号之间引用变量

    可以采用   ' +  变量 +  ' .

  2. LeetCode OJ--Insert Interval **

    https://oj.leetcode.com/problems/insert-interval/ 给出有序的区间来,再插入进去一个,也就是区间合并. 刚开始确立了几个思路,看要插入的区间的start ...

  3. Group by 两表联查取另一表与之关联数据的总数

    使用group by 需要查询的字段 必须要放在group by 后面 SELECT U.UserLotterySn,count(W.Userlotterysn) as WinCount,U.Acti ...

  4. AC日记——Andryusha and Socks Codeforces 780a

    A. Andryusha and Socks time limit per test 2 seconds memory limit per test 256 megabytes input stand ...

  5. 牛客网 牛客练习赛11 A.假的线段树

    看不懂题意,而且太菜,写了两道就溜了... A.假的线段树 链接:https://www.nowcoder.com/acm/contest/59/A来源:牛客网 时间限制:C/C++ 1秒,其他语言2 ...

  6. 缓存区溢出漏洞工具Doona

    缓存区溢出漏洞工具Doona   Doona是缓存区溢出漏洞工具BED的分支.它在BED的基础上,增加了更多插件,如nttp.proxy.rtsp.tftp等.同时,它对各个插件扩充了攻击载荷,这里也 ...

  7. JD路径配置及myeclipse主题和提示设置

    1. JDKAN安装及环境变量配置 安装jdk,注意记住安装路径(F:\Java\jdk1.8.0_121 )(个人爱好) 系统变量→新建 JAVA_HOME 变量 . 变量值填写jdk的安装目录(F ...

  8. 邁向IT專家成功之路的三十則鐵律 鐵律二十三:IT人的成家之道-樸實

    根據內政部一份2013年最新的調查報告指出台灣人的離婚率位居全球第三,想想看如果這是經濟成長率的排名表現那該有多好.然而究竟為何在台灣這塊小小的土地上,不僅離婚非常高而且晚婚的人也非常的多,其原因肯定 ...

  9. Android Studio 删除项目

    在项目上右键 点击“Open Module Settings”,然后你会看到你的项目排成一列,如果想删除哪个,点击项目,然后在左上角,点击“-”号,然后返回后发现这个项目变为灰色,点击项目右键,看到“ ...

  10. 使用find命令查找Linux中的隐藏文件的方法

    我们可以在Linux 或者 Unix 系统上使用 find 命令去查询所有(全部)隐藏文件 基本语法如下: 复制代码 代码如下: [root@dabu.info ~]#find /要查找的文/件/夹/ ...