- 回顾linux网络IO
- 遗留问题
- Netty特点
- 异步非阻塞通信
- 零拷贝
- 内存池
- 总结
- 内存分配流程
- 内存释放
- 个人看法
- 总结
- 高效的 Reactor 线程模型
- 无锁化的串行设计理念
- 高效的并发编程
- 高性能的序列化框架
- 灵活的 TCP 参数配置能力
epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。
这两个术语还挺抽象的,其实它们的区别还是很好理解的。
-
使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
-
使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;
在 IO 编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者 IO 多路复用技术进行处理。
Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 IO 阻塞导致的线程挂起。另外,由于 Netty 采用了异步通信模式,一个 IO 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 IO 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
零拷贝Netty 的“零拷贝”主要体现在如下三个方面:
-
Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行 Socket 读写,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
-
Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer。
-
Netty 的文件传输采用了
随着 JVM 虚拟机和 JIT 即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区 Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制
netty内存池的层级结构,主要分为
PoolArena
ChunkList
Chunk
Page
Subpage
按顺序依次访问q050、q025、q000、qInit、q075,遍历PoolChunkList内PoolChunk链表判断是否有PoolChunk能分配内存
宏观上来说,Netty对内存的管理分为两个层面。在为线程分配内存的过程中,会首先查找线程私有缓存(默认为线程开启缓存,可配置关闭),当私有缓存不满足需求时,会在内存池中查找合适的内存空间,提供给线程。:
- 线程私有缓存 Cache 线程私有缓存因为可以避免多线程请求内存时的竞争,所以效率很高,但是也存在一些缺陷:最大缓存容量小,每个线程默认32k;使用不当可能会造成内存泄漏.
- 全局的内存 Arena 全局共享的内存池支持堆内存和堆外内存(Direct Buffer)的申请和回收,其内存管理的粒度有以下几个单位:
- Chunk:Netty向操作系统申请内存是以Chunk为单位申请的,内存分配也是基于Chunk。Chunk是Page为单元的集合,默认16M。
- Page: 内存管理的最小单元,默认8K
- SubPage: Page内内存分配单元。
Netty逻辑上将内存大小分为了tiny, small, normal, huge 几个单位。申请内存大于Chunk size 为huge,此时不在内存池中管理,由JVM负责处理;当Client申请的内存大于一个Page的大小(normal), 在Chunk内进行分配; 对tiny&small大小的内存,在一个Page页内进行分配。针对上述几种粒度的内存块的管理,其实现上包含以下几个组件(类):
- PoolArena:内存分配中心
- PoolChunk:负责Chunk内的内存分配
- PoolSubpage:负责Page内的内存分配
一个chunk默认由211个页面构成,因为一个page 8k,所以默认完全二叉树11层。
Netty内存池分配出来的内存空间不是Client申请size的大小,而是大于size的最小2的幂次方(size > 512)或者是16的倍数。比如Client申请100byte的内存,那么返回的将是128byte。Netty会在入口处对申请的大小统一做规整化的处理,来保证分配出来的内存都是2的幂次方,这样做有两点好处:内存保持对齐,不会有很零散的内存碎片,这点和操作系统的内存管理类似;其次可以基于2的幂次方在二进制上的特性,大量运用位运算提升效率。后面的详细流程中我们将会看到。
Netty使用两个字节数组来表示两棵二叉树
memoryMap
存放分配信息- 节点编号从1开始,省略0是因为这样更容易计算父子关系: 子节点加倍,父节点减半
- 节点上的数字作为数组索引即id
- 随着节点分配值改变
depthMap
存放节点的高度信息- 节点编号从0开始
- 节点上的数字作为值
- 初始状态时,memoryMap和depthMap相等, 即
memoryMap[512] = depthMap[512] = 9
- 初始化后值不再改变
-
private final byte[] memoryMap; //表示完全二叉树,共有4096个
-
private final byte[] depthMap; //表示节点的层高,共有4096个
- memoryMap[i] = depthMap[i]:表示该节点下面的所有叶子节点都可用,这是初始状态
- memoryMap[i] = depthMap[i] + 1:表示该节点下面有一部分叶子节点被使用,但还有一部分叶子节点可用
- memoryMap[i] = maxOrder + 1 = 12:表示该节点下面的所有叶子节点不可用
其实归根到底,就是解决以下问题:
- 内存池管理算法是怎么做到申请效率,怎么减少内存碎片
- 高负载下内存池不断扩展,如何做到内存回收
- 内存池跟对象池作为全局数据,在多线程环境下如何减少锁竞争
为了减少线程间的竞争,Netty会提前创建多个PoolArena(默认生成数量 = 2 * CPU核心数),当线程首次请求池化内存分配,会找被最少线程持有的PoolArena,并保存线程局部变量PoolThreadCache中,实现线程与PoolArena的关联绑定(PoolThreadLocalCache#initialValue()方法)
Java自带的ThreadLocal实现线程局部变量的原理是:基于Thread的ThreadLocalMap类型成员变量,该变量中map的key为ThreadLocal,value-为需要自定义的线程局部变量值。调用ThreadLocal#get()方法时,会通过Thread.currentThread()获取当前线程访问Thread的ThreadLocalMap中的值
Netty设计了ThreadLocal的更高性能替代类:FastThreadLocal,需要配套继承Thread的类FastThreadLocalThread一起使用,基本原理是将原来Thead的基于ThreadLocalMap存储局部变量,扩展为能更快速访问的数组进行存储(Object[] indexedVariables),每个FastThreadLocal内部维护了一个全局原子自增的int类型的数组index
其他:
池化后内存的申请跟释放必然是成对出现的,那么如何做内存泄漏检测,特别是跨线程之间的申请跟释放是如何处理的?
因为采取线程级cache机制,涉及到线程结束时对象的释放,其本质机制是虚引用。从ThreadLocal、WeakHashMap(Map)都是虚引用的用法
总结 内存分配流程
Netty进行分配时,首先从对象池获取ByteBuf,之后从线程本地缓存MemoryRegionCache中查找内存页,再从Arena的内存池中查找,最后查找Chunk,分配SubPage,最后初始化Bytebuf对象。
从线程的本地缓存中获取PoolThreadCache对象,如果没有,则选择使用空间最少的Arena创建PoolThreadCache实例并保存至线程本地
使用PoolThreadCache的Arena从对象池中获取ByteBuf,对象池中默认没有释放的对象,会创建新对象。Arena根据HAS_UNSAFE判断是PooledUnsafeHeapByteBuf还是PooledHeapByteBuf进行相应处理。
根据请求的内存大小,判断其规格:tiny/small/normal/huge
- 若大小为tiny(0,512),从本地缓存的PoolThreadCache的MemoryRegionCache中查看是否有释放后的内存可以重用,若有则初始化PooledByteBuf;本地缓存池中没有可重用内存,先根据大小定位到在Arena的tinySubpagePools的位置idx,然后在其中查找可用的PoolSubpage,如果找到内存页,则使用内存页的chunk的初始化PoolSubpage。
- 若大小为small[512,pagesize],逻辑与tiny类似
- tiny和small在MemoryRegionCache和tinySubpagePools/smallSubpagePools中未找到可用分配的内存页则会调用allocateNormal寻找chunk
- 若大小为normal(pagesize,chunksize],会先从MemoryRegionCache中查找可用的回收后的缓存,如果未找到则会调用allocateNormal寻找chunk
- 寻找chunk时,先从q050->q025->q000->qInit->q075查找可用的chunk,如果没有找到,会创建PoolChunk对象的实例,创建chunk时会分配实际内存(heap使用byte[],direct使用ByteBuffer.allocateDirect)。找到chunk后,在chunk中查找或创建内存页,最后返回一个Handle。Handle是一个long型整数,记录了chunk和内部的偏移。
chunk寻找内存页的过程:根据请求大小计算idx,并找到tinySubpagePools或smallSubpagePools中idx位置的链表head;再根据大小计算在chunk的memoryMap叶子节点的index,如果chunk的subpages数组中index位置为空,说明没有创建PoolSubpage,创建新的内存页之后,并将其加入到chunk的subpages数组。最后修改subpage中的bitmap,讲该内存页加入到tinySubpagePools或smallSubpagePools对应位置的链表中。最后计算出分配出的内存的Handle,0x4000000000000000L | (long) bitmapIdx
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?