ByteBuf(图解1)
Netty ByteBuf(图解 )之一
疯狂创客圈 Java 分布式聊天室【 亿级流量】实战系列之15 【 博客园 总入口 】
源码工程
源码IDEA工程获取链接:Java 聊天室 实战 源码
写在前面
大家好,我是作者尼恩。
今天是百万级流量 Netty 聊天器 打造的系列文章的第15篇,这是一个基础篇。
由于关于ByteBuf的内容比较多,分两篇文章:
第一篇:图解 ByteBuf的分配、释放和如何避免内存泄露
第二篇:图解 ByteBuf的具体使用
本篇为第一篇。
Netty ByteBuf 优势
Netty 提供了ByteBuf,来替代Java NIO的 ByteBuffer 缓,来操纵内存缓冲区。
与Java NIO的 ByteBuffer 相比,ByteBuf的优势如下:
Pooling (池化,这点减少了内存复制和GC,提升效率)
可以自定义缓冲类型
通过一个内置的复合缓冲类型实现零拷贝
扩展性好,比如 StringBuffer
不需要调用 flip()来切换读/写模式
读取和写入索引分开
方法链
引用计数
手动获取与释放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源码。
截取的代码如下所示:
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter
{
//...
@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);
}
}
}
源码中,执行完重写的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的源代码截取如下:
String allocType = SystemPropertyUtil.get("io.netty.allocator.type", "unpooled").toLowerCase(Locale.US).trim();
Object alloc;
if("unpooled".equals(allocType)) {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else if("pooled".equals(allocType)) {
alloc = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: {}", allocType);
} else {
alloc = UnpooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: unpooled (unknown: {})", allocType);
}
不同的Netty版本,源码不一样。
上面的代码,是4.0版本的源码,默认为UnpooledByteBufAllocator。
而4.1 版本,默认为 PooledByteBufAllocator。因此,4.1版本的代码,是和上面的代码稍微有些不同的。
设置通道Channel的分配器
在4.x版本中,UnpooledByteBufAllocator是默认的allocator,尽管其存在某些限制。
现在PooledByteBufAllocator已经广泛使用一段时间,并且我们有了增强的缓冲区泄漏追踪机制,所以是时候让PooledByteBufAllocator成为默认了。
ServerBootstrap b = new ServerBootstrap()
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(port)
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(...);
}
});
使用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) 聊天程序【 亿级流量】实战 开源项目实战
- Netty 源码、原理、JAVA NIO 原理
- Java 面试题 一网打尽
- 疯狂创客圈 【 博客园 总入口 】
ByteBuf(图解1)的更多相关文章
- Netty ByteBuf(图解之 2)| 秒懂
目录 Netty ByteBuf(图解二):API 图解 源码工程 写在前面 ByteBuf 的四个逻辑部分 ByteBuf 的三个指针 ByteBuf 的三组方法 ByteBuf 的引用计数 Byt ...
- Netty(一):ByteBuf读写过程图解
我们知道ByteBuf通过读写两个索引分离,避免了NIO中ByteBuffer中读写模式切换时,需要flip等繁琐的操作. 今天就通过一段测试代码以及图例来直观的了解下ByteBuf中的readInd ...
- Redis分布式锁 (图解-秒懂-史上最全)
文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...
- 图解CSS3制作圆环形进度条的实例教程
圆环形进度条制作的基本思想还是画出基本的弧线图形,然后CSS3中我们可以控制其旋转来串联基本图形,制造出部分消失的效果,下面就来带大家学习图解CSS3制作圆环形进度条的实例教程 首先,当有人说你能不能 ...
- 《图解HTTP》读书笔记
目前国内讲解HTTP协议的书是在太少了,记忆中有两本被誉为经典的书<HTTP权威指南>与<TCP/IP详解,卷1>,但内容晦涩难懂,学习难度较大.其实,HTTP协议并不复杂,理 ...
- [PostgreSQL] 图解安装 PostgreSQL
图解安装 PostgreSQL [博主]反骨仔 [原文地址]http://www.cnblogs.com/liqingwen/p/5894462.html 序 园友的一篇<Asp.Net Cor ...
- 图解ios程序生命周期
图解ios程序生命周期 应用程序启动后状态有Active.Inactive.Background.Suspended.Not running这5种状态,几种状态的转换见下图: 在AppDelegate ...
- 【用xocde5打包 在IOS7以下也能显示无默认gloss 效果 图解】
图解,只需要这几个地方修改以下就好了,看不懂的话,请留言 完毕:记着clean 以下项目文件
- Python开发工具PyCharm个性化设置(图解)
Python开发工具PyCharm个性化设置,包括设置默认PyCharm解析器.设置缩进符为制表符.设置IDE皮肤主题等,大家参考使用吧. JetBrains PyCharm Pro 4.5.3 中文 ...
随机推荐
- jquery 日期插件
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- jQuery时间验证和转换为标准格式的时间
var TimeObjectUtil; /** * @title 时间工具类 * @note 本类一律违规验证返回false * @author {boonyachengdu@gmail.com} * ...
- AC日记——[CQOI2014]危桥 洛谷 P3163
题目描述 Alice和Bob居住在一个由N座岛屿组成的国家,岛屿被编号为0到N-1.某些岛屿之间有桥相连,桥上的道路是双向的,但一次只能供一人通行.其中一些桥由于年久失修成为危桥,最多只能通行两次.A ...
- js-斐波那切数列
f(1) = 1; f(2) = 1; f(3) = f(1) + f(2) = 2; f(4) = f(3) + f(2) = 3; f(5) = f(4) + f(3) = 5; f(6) = f ...
- Codefroces Gym101572 I.Import Spaghetti-有向图跑最小环输出路径(Floyd)
暑假学的很多东西,现在都忘了,补这道题还要重新学一下floyd,有点难过,我暑假学的东西呢??? 好了,淡定,开始写题解. 这个题我是真的很难过啊,输入简直是有毒啊(内心已经画圈诅咒出题人无数次了.. ...
- HDU 5794 A Simple Chess(杨辉三角+容斥原理+Lucas定理)
题目链接 A Simple Chess 打表发现这其实是一个杨辉三角…… 然后发现很多格子上方案数都是0 对于那写可能可以到达的点(先不考虑障碍点),我们先叫做有效的点 对于那些障碍,如果不在有效点上 ...
- [原创][FPGA]Quartus实用小技巧(长期更新)
0. 简介 在使用Quartus软件时,经常会时不时的发现一些小技巧,本文的目的是总结所查阅或者发现到的小技巧,本文长期更新. 1. Quartus中的模板功能 最近在Quartus II的菜单里找到 ...
- [Python Cookbook] Numpy: How to Apply a Function to 1D Slices along the Given Axis
Here is a function in Numpy module which could apply a function to 1D slices along the Given Axis. I ...
- 利用注解和反射,将Bean枚举字段的值填入相应的字段中,并转化为fastjson返回前台
需求:需要将枚举类型的字段例如enable(是否启用)转化为enable:1,enableName:是.这种形式返回给前台. 思路:在bean字段上加上枚举类型的注解,通过字段的值和枚举类反射获取枚举 ...
- soursTree新建过程.md
网上博客 https://www.cnblogs.com/tian-xie/p/6264104.html 主要的推送流程 完成所有项目的远程推送工作 点击git工作流选择第二个建立新的版本; 输入发布 ...