ByteBuf
创建
1、ByteBufAllocator
(1)实现类负责分配缓冲区,这个接口的实现应该是线程安全的
(2)默认实现
ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;
static final ByteBufAllocator DEFAULT_ALLOCATOR;
(3)当 ByteBuf 容量无法容纳所有数据时,ByteBuf 会进行扩容操作
2、堆内存
ByteBuf heapBuffer()
ByteBuf heapBuffer(int initialCapacity)
ByteBuf heapBuffer(int initialCapacity, int maxCapacity)
3、直接内存
(1)创建、销毁的代价昂贵,但读写性能高(少一次内存复制),适合配合池化功能
(2)对 GC 压力小,因为直接内存不受 JVM 垃圾回收的管理,需要及时主动释放
ByteBuf directBuffer()
ByteBuf directBuffer(int initialCapacity)
ByteBuf directBuffer(int initialCapacity, int maxCapacity)
4、创建
(1)使用 ChannelHandlerContext 调用 alloc(),获取 ByteBufAllocator,而不是使用 ByteBufAllocator.DEFAULT
(2)调用以下方法
ByteBuf buffer()
ByteBuf buffer(int initialCapacity)
ByteBuf buffer(int initialCapacity, int maxCapacity)
(3)默认情况下,根据不同平台,选择池化 / 非池化
static {
//获取Java系统配置,以io.netty.allocator.type为准
//默认情况下,若为安卓平台,则为unpooled;非安卓平台,为pooled
String allocType = SystemPropertyUtil.get("io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
allocType = allocType.toLowerCase(Locale.US).trim();
ByteBufAllocator 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 = PooledByteBufAllocator.DEFAULT;
logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
}
DEFAULT_ALLOCATOR = alloc;
……
}
(4)若不指定初始容量,则为 256
DEFAULT_SMALL_CACHE_SIZE = SystemPropertyUtil.getInt("io.netty.allocator.smallCacheSize", 256);
(5)默认情况下,不论池化 / 非池化,都创建直接内存
public static final UnpooledByteBufAllocator DEFAULT = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
public static final PooledByteBufAllocator DEFAULT = new PooledByteBufAllocator(PlatformDependent.directBufferPreferred());
public static boolean directBufferPreferred() {
return DIRECT_BUFFER_PREFERRED;
}
private static final boolean DIRECT_BUFFER_PREFERRED;
//获取Java系统配置
DIRECT_BUFFER_PREFERRED = CLEANER != NOOP && !SystemPropertyUtil.getBoolean("io.netty.noPreferDirect", false);
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.noPreferDirect: {}", !DIRECT_BUFFER_PREFERRED);
}
池化
1、可以重用 ByteBuf,采用与 jemalloc 类似的内存分配算法,提升分配效率
2、高并发时,池化更节约内存,减少内存溢出的可能
3、没有池化,则每次都需要创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,即使是堆内存,也会增加 GC 压力
4、设置系统环境变量来设置
-Dio.netty.allocator.type={unpooled|pooled}
(1)unpooled:非池化
(2)pooled:池化
(3)若不指定,使用 DEFAULT,则为池化
5、版本、平台
(1)4.1 以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现
(2)4.1 之前,池化功能不成熟,默认为非池化实现
四部分组成
1、最大容量、当前容量
(1)在构造 ByteBuf 时,可传入两个参数,分别代表初始容量、最大容量,若未指定最大容量,最大容量默认为 Integer.MAX_VALUE
(2)当 ByteBuf 容量无法容纳所有数据时,会进行扩容操作,若超出最大容量,会抛出 java.lang.IndexOutOfBoundsException 异常
2、读、写指针
(1)读写操作不同于 ByteBuffer 只用 position 进行控制,ByteBuf 分别由读指针、写指针控制
(2)进行读写操作时,无需进行模式的切换
(3)废弃部分:读指针之前的部分,是已经读过的内容
(4)可读部分:读指针与写指针之间的空间
(5)可写部分:写指针与当前容量之间的空间
写入
1、以下方法的返回值都是 ByteBuf,意味着可以链式调用,来写入不同数据
2、网络传输中,默认使用 Big Endian,使用 writeInt(int value)
3、set 开头的一系列方法,可以写入数据,但不会改变写指针位置
方法 | 含义 | 备注 |
---|---|---|
writeBoolean(boolean value) | 写入 boolean 值 | 用一字节 01|00 代表 true|false |
writeByte(int value) | 写入 byte 值 | |
writeShort(int value) | 写入 short 值 | |
writeInt(int value) | 写入 int 值 | Big Endian(大端写入),例:0x250,写入后 00 00 02 50 |
writeIntLE(int value) | 写入 int 值 | Little Endian(小端写入),例:0x250,写入后 50 02 00 00 |
writeLong(long value) | 写入 long 值 | |
writeChar(int value) | 写入 char 值 | |
writeFloat(float value) | 写入 float 值 | |
writeDouble(double value) | 写入 double 值 | |
writeBytes(ByteBuf src) | 写入 Netty 的 ByteBuf | |
writeBytes(byte[] src) | 写入 byte[] | |
writeBytes(ByteBuffer src) | 写入 NIO 的 ByteBuffer | |
int writeCharSequence(CharSequence sequence, Charset charset) | 写入字符串 | CharSequence 为字符串类的父类,Charset 为对应的字符集 |
扩容规则
1、若写入后数据大小,未超过 512 字节,则选择 16 倍数的最小整数进行扩容
2、若写入后数据大小超过 512 字节,则选择 2n 的最小整数
3、扩容不能超过 maxCapacity,否则会抛出 java.lang.IndexOutOfBoundsException 异常
读取
1、通过一系列 read 方法进行读取,读取时会根据读取数据的字节数,移动读指针
2、如果需要重复读取
(1)调用 byteBuf.markReaderIndex(),对读指针进行标记
(2)调用 byteBuf.resetReaderIndex(),将读指针恢复到 mark 标记的位置
3、以 get 开头的一系列方法,可以读取数据,但不会改变读指针的位置
retain、release
1、由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等待 GC 垃圾回收
(1)UnpooledHeapByteBuf:使用 JVM 内存,只需等待 GC 回收内存即可
(2)UnpooledDirectByteBuf:使用直接内存,需要特殊的方法来回收内存
(3)PooledByteBuf 及其子类使用池化机制,需要更复杂的规则来回收内存
回收内存的源码实现,请关注下面方法的不同实现
protected abstract void deallocate()
2、Netty 采用引用计数法来控制回收内存
(1)每个 ByteBuf 都实现 ReferenceCounted 接口
(2)每个 ByteBuf 对象的初始计数为 1
(3)调用 release 方法,计数减 1,如果计数为 0,ByteBuf 内存被回收
(4)调用 retain 方法,计数加 1,若计数不为 0,即使其它 handler 调用 release,也不会造成回收
(5)当计数为 0 时,底层内存会被回收,即使 ByteBuf 对象存在,其各个方法均无法正常使用
释放规则
1、因为存在 ChannelPipeline,一般需要将 ByteBuf 传递给下一个 ChannelHandler
(1)如果在每个 ChannelHandler 中都去调用 release ,就失去传递性
(2)如果在 ChannelHandler 内,ByteBuf 已完成任务,那么便无须再传递
2、基本规则
(1)最后使用 ByteBuf 的调用者,就负责 release
3、入站 ByteBuf 处理原则
(1)对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,此时不需要 release
(2)将原始 ByteBuf 转换为其它类型的 Java 对象,此时 ByteBuf 不使用,必须 release
(3)如果不调用 ctx.fireChannelRead(msg) 向后传递,必须 release
(4)注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release
(5)假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)
4、出站 ByteBuf 处理原则
(1)出站消息最终都会转为 ByteBuf 输出,一直向前传,HeadContext 先 flush 再 release
5、异常处理原则
(1)不清楚 ByteBuf 被引用多少次的情况下,但又必须彻底释放
(2)可以循环调用 release(),直到返回 true
(3)通过 refCnt() 获取当前的引用计数,然后调用 release(int refCnt) 释放
tail、head 自动释放
1、入站处理器
final TailContext tail;
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler
(1)入站处理器传递内容需要调用 channelRead
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
onUnhandledInboundMessage(ctx, msg);
}
protected void onUnhandledInboundMessage(ChannelHandlerContext ctx, Object msg) {
onUnhandledInboundMessage(msg);
if (logger.isDebugEnabled()) {
logger.debug("Discarded message pipeline : {}. Channel : {}.",
ctx.pipeline().names(), ctx.channel());
}
}
protected void onUnhandledInboundMessage(Object msg) {
try {
logger.debug(
"Discarded inbound message {} that reached at the tail of the pipeline. " +
"Please check your pipeline configuration.", msg);
} finally {
//自定释放内存
ReferenceCountUtil.release(msg);
}
}
2、出栈处理器
final HeadContext head;
final class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler
(1)出站处理器传递参数需要调用 write
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
unsafe.write(msg, promise);
}
(2)AbstractChannel.AbstractUnsafe
@Override
public final void write(Object msg, ChannelPromise promise) {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
try {
//自动释放内存
ReferenceCountUtil.release(msg);
} finally {
// If the outboundBuffer is null we know the channel was closed and so
// need to fail the future right away. If it is not null the handling of the rest
// will be done in flush0()
// See https://github.com/netty/netty/issues/2362
safeSetFailure(promise,
newClosedChannelException(initialCloseCause, "write(Object, ChannelPromise)"));
}
return;
}
int size;
try {
msg = filterOutboundMessage(msg);
size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
try {
ReferenceCountUtil.release(msg);
} finally {
safeSetFailure(promise, t);
}
return;
}
outboundBuffer.addMessage(msg, size, promise);
}
3、最终调用 ReferenceCountUtil.release(msg) 释放内存
public static boolean release(Object msg) {
//只有实现ReferenceCounted接口,才能被释放
if (msg instanceof ReferenceCounted) {
return ((ReferenceCounted) msg).release();
}
return false;
}
public static boolean release(Object msg, int decrement) {
ObjectUtil.checkPositive(decrement, "decrement");
//只有实现ReferenceCounted接口,才能被释放
if (msg instanceof ReferenceCounted) {
return ((ReferenceCounted) msg).release(decrement);
}
return false;
}
slice 方法
1、零拷贝体现之一
2、对原始 ByteBuf 进行切片成多个 ByteBuf
(1)切片后的 ByteBuf 并没有发生内存复制,仍使用原始 ByteBuf 内存
(2)切片后的 ByteBuf 维护独立的 read、write 指针
(3)得到分片后的 ByteBuf,要调用其 retain 方法,使其内部的引用计数 + 1,避免原 ByteBuf 释放,导致切片 ByteBuf 无法使用
(4)修改原 ByteBuf 中的值,会影响切片后的 ByteBuf
(5)切片后的 ByteBuf 的 maxCapacity 被固定,因此不能追加 write
duplicate 方法
1、零拷贝体现之一
2、映射原始 ByteBuf 所有内容
(1)没有 maxCapacity 限制
(2)与原始 ByteBuf 使用同一块底层内存
(3)读、写指针是独立的
(4)调用其 retain 方法,使其内部的引用计数 + 1,避免原 ByteBuf 释放,导致复制 ByteBuf 无法使用
copy 方法
1、对底层内存数据进行深拷贝
2、读写都与原始 ByteBuf 无关
CompositeByteBuf
1、零拷贝体现之一
2、一个虚拟缓冲区,将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免拷贝
3、建议使用 ByteBufAllocator.compositeBuffer() 或 Unpooled.wrappedBuffer(ByteBuf...),而不是明确调用构造函数
4、CompositeByteBuf 是一个组合 ByteBuf
(1)内部维护一个 Component 数组,每个 Component 管理一个 ByteBuf,记录这个 ByteBuf 相对于整体偏移量等信息,代表着整体中某一段的数据
(2)优点:对外是一个虚拟视图,组合多个 ByteBuf 不会产生内存复制
(3)缺点:复杂实现,多次操作会带来性能损耗
(4)调用其 retain 方法,使其内部的引用计数 + 1,避免原 ByteBuf 释放,导致组合 ByteBuf 无法使用
Unpooled
1、工具类:提供非池化的 ByteBuf 创建、组合、复制等操作
2、wrappedBuffer 方法
(1)零拷贝相关,底层不会有拷贝操作
(2)包装 ByteBuf / ByteBuffer / byte[],返回 ByteBuf
(3)调用其 retain 方法,使其内部的引用计数 + 1,避免原 ByteBuf 释放,导致组合 ByteBuf 无法使用
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战