Netty作为一款高性能网络应用程序框架,实现了一套高性能内存管理机制
通过学习其中的实现原理、算法、并发设计,有利于我们写出更优雅、更高性能的代码;当使用Netty时碰到内存方面的问题时,也可以更高效定位排查出来
本文基于Netty4.1.43.Final介绍其中的内存管理机制
ByteBuf分类
Netty使用ByteBuf对象作为数据容器,进行I/O读写操作,Netty的内存管理也是围绕着ByteBuf对象高效地分配和释放
当讨论ByteBuf对象管理,主要从以下方面进行分类:
- Pooled 和 Unpooled
Unpooled,非池化内存每次分配时直接调用系统 API 向操作系统申请ByteBuf需要的同样大小内存,用完后通过系统调用进行释放Pooled,池化内存分配时基于预分配的一整块大内存,取其中的部分封装成ByteBuf提供使用,用完后回收到内存池中
tips: Netty4默认使用Pooled的方式,可通过参数-Dio.netty.allocator.type=unpooled或pooled进行设置
- Heap 和 Direct
Heap,指ByteBuf关联的内存JVM堆内分配,分配的内存受GC 管理
Direct,指ByteBuf关联的内存在JVM堆外分配,分配的内存不受GC管理,需要通过系统调用实现申请和释放,底层基于Java NIO的DirectByteBuffer对象
note: 使用堆外内存的优势在于,Java进行I/O操作时,需要传入数据所在缓冲区起始地址和长度,由于GC的存在,对象在堆中的位置往往会发生移动,导致对象地址变化,系统调用出错。为避免这种情况,当基于堆内存进行I/O系统调用时,需要将内存拷贝到堆外,而直接基于堆外内存进行I/O操作的话,可以节省该拷贝成本
池化(Pooled)对象管理
非池化对象(Unpooled),使用和释放对象仅需要调用底层接口实现,池化对象实现则复杂得多,可以带着以下问题进行研究:
- 内存池管理算法是如何实现高效内存分配释放,减少内存碎片
- 高负载下内存池不断申请/释放,如何实现弹性伸缩
- 内存池作为全局数据,在多线程环境下如何减少锁竞争
1 算法设计
1.1 整体原理
Netty先向系统申请一整块连续内存,称为chunk,默认大小chunkSize = 16Mb,通过PoolChunk对象包装。为了更细粒度的管理,Netty将chunk进一步拆分为page,默认每个chunk包含2048个page(pageSize = 8Kb)
不同大小池化内存对象的分配策略不同,下面首先介绍申请内存大小在(pageSize/2, chunkSize]区间范围内的池化对象的分配原理,其他大对象和小对象的分配原理后面再介绍。在同一个chunk中,Netty将page按照不同粒度进行多层分组管理:
- 第1层,分组大小size = 1*pageSize,一共有2048个组
- 第2层,分组大小size = 2*pageSize,一共有1024个组
- 第3层,分组大小size = 4*pageSize,一共有512个组
...
当请求分配内存时,将请求分配的内存数向上取值到最接近的分组大小,在该分组大小的相应层级中从左至右寻找空闲分组例如请求分配内存对象为1.5 pageSize,向上取值到分组大小2 pageSize,在该层分组中找到完全空闲的一组内存进行分配,如下图: