总结
Bytebuf需要释放,否则可能导致OOM。
如果bytebbuf传递到了head或tail,不需要我们关心。
在head和tail里(head实现了outhandler、inhander。tail实现了inhander),底层自动调用了bytebuf.release。
其他情况需要我们手动释放。 注意simpleinboundHandler特殊
netty默认使用的直接内存(也叫非堆内存或堆外内存),它创建、销毁的开销同比堆内存大,所以用池化来复用,如果使用直接内存而不用池化,性价比可能还不如堆内存。
直接内存可以减少用户态和内核态间的一次数据复制,所以在IO网络中优势高。如果不涉及IO网络操作,只是单纯操作内存,使用堆内存更好。
池化也能用在堆内存上,所以一共有四种:池化直接内存、池化堆内存、非池化直接内存、非池化堆内存。
ByteBuf模型
使用时会自动扩容,可扩容部分就是下图紫色。
创建ByteBuf
ByteBuf poolHeapBuf = ByteBufAllocator.DEFAULT.heapBuffer();
ByteBuf poolDiredtBuf = ByteBufAllocator.DEFAULT.directBuffer();
ByteBuf unpoolHeapBuf = Unpooled.buffer();
ByteBuf unpoolDirBuf = Unpooled.directBuffer();
System.out.println(poolHeapBuf);
System.out.println(poolDiredtBuf);
System.out.println(unpoolHeapBuf);
System.out.println(unpoolDirBuf);
前两种是否是池化,需要看-Dio.netty.allocator.type=unpooled|pooled
,如果是pooled,则二者都是池化内存。
后两种一定是非池化内存。
参数为pooled时,输出如下。实际中,还可以用ChannelHandlerContext ctx来分配。
PooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 256)
PooledUnsafeDirectByteBuf(ridx: 0, widx: 0, cap: 256)
UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 256)
UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(ridx: 0, widx: 0, cap: 256)
李林峰在书中指出,每次默认开一个64KB大小的数组,导致了OOM。
结合代码分析发现,API网关每次收到请求消息,无论请求消息大还是小,哪怕只有100字节,都会构造一个默认为64KB的char数组,用于处理和转发请求消息。如果后端转发消息较慢,就会导致任务队列积压,由于每个任务(Runnable,此处为 Lambda 表达式)持有一个64KB的char数组,所以积压多了就会转移到老年代,一旦触发老年代GC,耗时较长,就会导致系统吞吐量降为0。
netty原码释放逻辑
当多个handler串起来时,数据会沿着链传统。用inboundhandler举例。
如何唤起下一个inhandler呢?可以调用fireChannelRead
class ServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
}
底层就是:
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
return this;
}
findContextInbound就是找到下一个handler。
invokeChannelRead就是调用下一个handler的invokeChannelRead,代码如下。
始终让Niothread去执行invokeChannelRead。并且执行的最后一个肯定是tail。
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor(); // 该context所属的线程
if (executor.inEventLoop()) {
next.invokeChannelRead(m);
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}
然后执行tail里面的代码
尤其是看看注释是啥:用户在channelRead里面没有处理message 并且该message传递到了tail节点,那么tail节点就会负责释放内存。
/**
* Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
* in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
* to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
*/
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);
}
}
为什么始终让Niothread去执行invokeChannelRead?
假如我们这样做,Netty还是会交给NIO线程去释放。
new Thread(() -> {
ctx.fireChannelRead(msg);
}).start();
Netty 中的 invokeChannelRead 方法必须在 NIO 线程池中执行,以确保网络读事件的处理是高效、可靠和非阻塞的。
simpleinboundHandler
他为什么特殊?他在自己的finally快里面就释放了
@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);
}
}
}
所以如果实现了sampleinboundhandler,byteBuf不用手动释放,也不用传递到tail去。
手动释放
调用release()
方法即可。
有特别注意的点。
ByteBuf的一些方法如slice()、duplicate、compositeByteBuf等,都是基于原数组的引用。
如果你释放掉了,其他人持有该引用写入就会发生问题。
最好是每调用上述方法一次,就引用次数+1retain()
,谁使用完,谁就release掉。
public static void main(String[] args) throws Exception {
ByteBuf poolHeapBuf = ByteBufAllocator.DEFAULT.heapBuffer(10);
// 模拟多人使用
use(poolHeapBuf);
use(poolHeapBuf);
}
private static void use(ByteBuf poolHeapBuf) {
ByteBuf slice = poolHeapBuf.slice(0, 3);
slice.retain();
// 执行逻辑完,最后释放掉。
slice.release();
}
再次理解netty释放逻辑
ReferenceCountUtil.release(msg);
底层调用的这个方法,
再次进入源码,发现只要实现了ReferenceCounted接口的,都会被释放掉。
public static boolean release(Object msg) {
if (msg instanceof ReferenceCounted) {
return ((ReferenceCounted) msg).release();
}
return false;
}
他的实现有很多,包括前面的各种byteBuf,还有http相关,使用时要注意
本文作者:WKP9418
本文地址:https://blog.csdn.net/qq_43179428/article/details/140564408