参考资料:
[1]. netty源码分析之揭开reactor线程的面纱(二)
[2]. Netty 源码分析之 一 揭开 Bootstrap 神秘的红盖头 (服务器端)
[3]. netty源码分析之揭开reactor线程的面纱(三)
[4]. netty源码分析之揭开reactor线程的面纱(一)
[5]. netty源码分析之pipeline(二)
[6]. Netty中的装饰者模式
[7]. 深入浅出Netty内存管理 PoolChunk
[8]. 禁用Netty对象缓存机制
[9]. ThreadLocal与弱引用WeakReference
[10]. ByteBuf继承结构
[11]. Netty 轻量级对象池Recycler分析
[12]. How to use io.netty.util.Recycler in multiple thread, will it cause a memory leak?
[13]. Netty源码笔记-第八章 内存管理
[14]. netty学习系列四:读操作
[15]. Netty-第八章 内存管理-1.基本概念
- SelectedKeys优化
在[1]中,最令我震撼的是netty为了提高效率,用反射selector的SelectedKeys从set的实现改为另外一个用数组实现的set
private SelectedSelectionKeySet selectedKeys;
private Selector NioEventLoop.openSelector() {
//...
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
// selectorImplClass -> sun.nio.ch.SelectorImpl
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
selectedKeysField.setAccessible(true);
publicSelectedKeysField.setAccessible(true);
selectedKeysField.set(selector, selectedKeySet);
publicSelectedKeysField.set(selector, selectedKeySet);
//...
selectedKeys = selectedKeySet;
}
- handler 与 childHandler 的区别与联系
在配置ServerBootstrap的时候有两个handler可以配置,handler 与 childHandler,前者在建立连接的时候在bossgroup起作用,后者连接之后在wokergroup起作用。
下面内容引用自[2]
最后我们来总结一下服务器端的 handler 与 childHandler 的区别与联系:
在服务器 NioServerSocketChannel 的 pipeline 中添加的是 handler 与 ServerBootstrapAcceptor.
当有新的客户端连接请求时, ServerBootstrapAcceptor.channelRead 中负责新建此连接的 NioSocketChannel 并添加 childHandler 到 NioSocketChannel 对应的 pipeline 中, 并将此 channel 绑定到 workerGroup 中的某个 eventLoop 中.
handler 是在 accept 阶段起作用, 它处理客户端的连接请求.
childHandler 是在客户端连接建立以后起作用, 它负责客户端连接的 IO 交互.
- reactor线程的机制
socket连接之后,要有worker线程对socket进行数据的读写和处理,线程的主要工作如下图所示,大致就是先读写事件,然后再运行一些数据处理的任务。
[3] 主要讲的是上图run tasks这块。
线程的父类SingleThreadEventExecutor中有taskQueue,创建的时候是调用NioEventLoop重写的newTaskQueue,这个mpsc是多生产者,单一消费者的线程,根据[3]中分析,其他的线程调用channel.write(...)
之类的可以将要执行的任务加入到对应线程的队列中。
@Override
protected Queue newTaskQueue(int maxPendingTasks) {
// This event loop never calls takeTask()
return PlatformDependent.newMpscQueue(maxPendingTasks);
}
[3]中作者有个问题问的很好:schedule为什么要使用优先级队列,而不需要考虑多线程的并发?
因为schedule任务的加入都是自己的线程完成的。
对此,netty的处理是,如果是在外部线程调用schedule,netty将添加定时任务的逻辑封装成一个普通的task,这个task的任务是添加[添加定时任务]的任务,而不是添加定时任务,其实也就是第二种场景,这样,对 PriorityQueue的访问就变成单线程,即只有reactor线程
最终结合[3]和[4],我们可以知道NioEventLoop其实结合了定时器任务队列和资源等待队列,后者用来等待socket注册的事件,每次阻塞之前都会处理所有的任务,处理完调用select等待,等待的超时事件为定时器第一个到期任务。
- 异常的传递
参考[5],pipline在这里就可以体现出好处,可以将异常很方便的进行传递。
在in的过程中,会调用每个handler都有的exceptionCaught方法,如果没有复写,则fireExceptionCaught,这样就会一直传递下去,如果自定义的handler都没有处理,最后如果有ChannelHandlerContext在它的exceptionCaught就有进行处理。
在out的过程也是一直向后传播,这里有点疑惑,为什么out的过程是向后传播而不是向前传播?单纯为了找ChannelHandlerContext?那out向前的那些handler的异常处理都没有调用到?
异常传播只会往后传播,而且不分inbound还是outbound节点,不像outBound事件一样会往前传播
- NioEventLoop 继承体系
1、NioEventLoop 可以被追溯到 java.util.concurrent.ExecutorService 接口。这个信息表明 NioEventLoop 能够执行 Runnable 任务,从而保证了这些要执行的任务是在 NioEventLoop 的线程中执行,而非外部线程来执行。NioEventLoop 内置的一些 Runnable 任务包括了对 channel 的 register、系统缓冲区满后而推迟的 flush 任务等。
- ByteBuf
ByteBuf分类:
内存的来源:堆内存(Heap)与直接(本地)内存(Direct)
是否使用池化技术:Unpooled
性能增强
UnpooledUnsafeDirectByteBuf和UnpooledUnsafeNoCleanerDirectByteBuf,内部使用了Unsafe进行高性能的get/set操作,为何高性能这里涉及Unsafe操作本地方法,NoCleaner涉及操作直接内存(非堆)时使用Unsafe本地方法创建和释放DirectByteBuffer不使用JDK内部实现的DirectByteBuffer创建和释放,因为JDK内部的DirectByteBuffer内部实现了Cleaner机制来释放内存,所以这里直接操作内存叫NoCleaner,这样做有两个好处:1.屏蔽了不同版本JDK的实现差异,但违背了跨平台特性。2.高性能直接操作内存,适配高并发场景。
ByteBuf提供了一些较为丰富的实现类,逻辑上主要分为两种:HeapByteBuf和DirectByteBuf,实现机制则分为两种:PooledByteBuf和UnpooledByteBuf,除了这些之外,Netty还实现了一些衍生ByteBuf(DerivedByteBuf),如:ReadOnlyByteBuf、DuplicatedByteBuf以及SlicedByteBuf。
UnpooledHeapByteBuf
- SimpleChannelInboundHandler内存释放
如果我们在构建SimpleChannelInboundHandler的时候将autoRelease
设置为真,也就是说,SimpleChannelInboundHandler内部帮忙释放内存的话,从下面的逻辑我们可以知道内存是在最后一个Handler里面释放的,前面的Handler如果调用输入的类型是I,那么会调用当前的channelRead0,然后释放内存;如果不是当前需要的类型,继续向下传递,由后面的Handler自己释放内存,这里不帮忙释放内存。
我们记住谁拉屎谁擦屁股就好,谁用到I类型的对象,谁自己去释放。
@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);
}
}
}
这里我们猜测,如果没有人用到这个变量,最后在TailContext中会释放内存,果不其然,channelRead->onUnhandledInboundMessage最后会释放内存。
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);
}
}
- PoolArena
Netty采用了jemalloc的思想,这是FreeBSD实现的一种并发malloc的算法。jemalloc依赖多个Arena来分配内存,运行中的应用都有固定数量的多个Arena,默认的数量与处理器的个数有关。系统中有多个Arena的原因是由于各个线程进行内存分配时竞争不可避免,这可能会极大的影响内存分配的效率,为了缓解高并发时的线程竞争,Netty允许使用者创建多个分配器(Arena)来分离锁,提高内存分配效率,当然是以内存来作为代价的。
-
PoolChunk与PoolSubpage
chunk=2048page
16MB=20488KB
参考[7],屏蔽细节的说法,一共有2048个page,每个page的大小一样,将所有的page当做树的叶子,用一棵完全二叉树维系了page的所有权,最上面的节点拥有全部的page,其他节点也拥有对应叶子个数的page。
如果分配的内存大于page,向上取整分配2次幂的page,二叉树每个节点都维系着其page(叶子)的使用情况——全部未使用,子节点拥有的最大完整page个数(2^N),全部使用完了。
如果分配的内存小于page,会使用PoolSubpage,PoolSubpage是将一个page切分为后面其中一个的等分——16/32/64.../512(tiny,32种)或者512/1024/2048/4096(small,4种),由第一次分配内存的时候进行切分,会找到对应的PoolSubpage,不会分配多块内存,比如分配31个字节,直接去找以32字节进行切分的PoolSubpage,不会去以16字节进行切分的PoolSubpage找两块连续的16拼起来。([])
运用这个数据结构的特点是:能够快速找到2次幂的page。
PoolThreadCache
PoolThreadCache来减少分配内存时的锁的争用,但如果使用这个机制,获取和释放内存的要在同一个线程,如果在IO线程中调用分配,在业务线程中调用释放,那么缓存的内存都放到业务线程中去,下次IO线程调用还得重新分配内存,它并没有缓存到内存。(参考[8])FastThreadLocal怎么Fast?
ThreadLocal可以看下[9]。
简单的来说FastThreadLocal就是将ThreadLocal的哈希表的映射去掉,每次都分配一个新的index给变量,根据这个变量在数据中心存取变量。Recycler
Recycler是一个用来回收经常重复创建和丢弃的对象,Recycler是属于某个线程的,从其主要用来回收的容器——一个netty自己实现的内部类Stack的定义就可以看出来,如下代码所示。
private final FastThreadLocal> threadLocal = new FastThreadLocal>() {
@Override
protected Stack initialValue() {
return new Stack(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor,
ratioMask, maxDelayedQueuesPerThread);
}
};
Recycler的用法如下所示,引用自[11]:
为什么回收的时候知道是否是Recycler的线程创建的?因为对象要使用Recycler进行回收,会有一定的使用规则,会传一个句柄跟对象绑定到一起,这个句柄绑定到Recycler和线程,回收的时候使用句柄进行回收,就可以知道。实际上Recycler.Handle使用起来还可以更简洁点,可以再封装作为一个类让对象来继承,实现装饰器模式,anyway,下面这样还是用起来了。Recycler.Handle是我们放进栈或者队列中的一个元素,其中的value域就是我们的对象。
public class RecyclerTest {
private static final Recycler RECYCLER = new Recycler() {
@Override
protected User newObject(Handle handle) {
return new User(handle);
}
};
public static class User {
private Recycler.Handle handle;
public User(Recycler.Handle handle) {
this.handle = handle;
}
public void recycle() {
handle.recycle(this);
}
}
public static void main(String[] args) throws InterruptedException {
User user = RECYCLER.get();
user.recycle(); // 同线程回收对象
// new FastThreadLocalThread(() -> {
// user.recycle();
// }).start(); // 异线程回收对象
System.out.println(user == RECYCLER.get()); // true
}
}
当我们使用完将对象回去回去后,Recycler内部的结构用了一个栈来回收自己线程创建的对象,多个WeakOrderQueue(其他线程每个对应一个队列)回收非自己线程创建的对象。从Recycler中获取对象的时候,首先从栈中获取,如果获取不到就从WeakOrderQueue从获取,如果还是获取不到最后从调用上面复写的抽象方法newObject获取对象。其实我不知道WeakOrderQueue用来干嘛?貌似是防止其他线程放入对象和自己线程取对象发生冲突,但是却不知道为什么WeakOrderQueue可以做到这点,因为获取的时候也可能到WeakOrderQueue进行获取?github上面[12]
有人提问,作者也回答了,但是看不懂。
- PooledByteBufAllocator实现
PoolThreadLocalCache和PoolThreadCache的名字相差了一个Local,这个Local代表了PoolThreadLocalCache是专属某个线程的一个PoolThreadCache缓冲池,可以从下面PoolThreadLocalCache类的定义代码中看出,其继承自FastThreadLocal,关于FastThreadLocal可以参考上面的内容,可以简单看做ThreadLocal的另外一个实现。
PoolThreadCache内部保存了tiny/small/normal的堆内存和直接内存的MemoryRegionCache数组。
final class PoolThreadLocalCache extends FastThreadLocal {
@Override
protected synchronized PoolThreadCache initialValue() {
final PoolArena heapArena = leastUsedArena(heapArenas);
final PoolArena directArena = leastUsedArena(directArenas);
return new PoolThreadCache(
heapArena, directArena, tinyCacheSize, smallCacheSize, normalCacheSize,
DEFAULT_MAX_CACHED_BUFFER_CAPACITY, DEFAULT_CACHE_TRIM_INTERVAL);
}
@Override
protected void onRemoval(PoolThreadCache threadCache) {
threadCache.free();
}
private PoolArena leastUsedArena(PoolArena[] arenas) {
if (arenas == null || arenas.length == 0) {
return null;
}
PoolArena minArena = arenas[0];
for (int i = 1; i < arenas.length; i++) {
PoolArena arena = arenas[i];
if (arena.numThreadCaches.get() < minArena.numThreadCaches.get()) {
minArena = arena;
}
}
return minArena;
}
}
- RecvByteBufAllocator([15])
RecvByteBufAllocator内部定义了一个Handle,用在底层读数据的时候实现高效分配内存,它实际上不进行具体的分配,只是根据前面分配的分配内存大小计算好下次分配需要的内存大小,然后根据传入的ByteBufAllocator分配内存。
底层写操作
https://www.cnblogs.com/stateis0/p/9062154.html