内存管理-PooledByteBufAllocator-Chunk
前言
前文介绍了 UnPooledByteBufAllocator 和 Huge,这两种内存使用方式不涉及自己管理内存,而是借助 jvm 或操作系统的内存管理来实现的。Chunk 级别的内存管理则是在 jvm 或操作系统的内存管理之上又做了一层内存管理,这就带来了一定的复杂性。我们知道现在 jvm 或操作系统提供的内存管理也是经过前人无数的总结实践挑选出来的比较优秀的内存管理方法,那为什么 Netty 还要自己再做一层内存管理呢?既然选择了做,Netty 又是怎么做的内存管理呢?下文以及后面的文章我们将逐步进行探索。
目标
1. 了解为什么自己管理内存
2. 了解 Cache 机制
3. 了解 Netty 在内存管理上的 Cache 实践以及线程安全实践
代码示例
下面这一段代码有助于我们跟代码的时候了解 Chunk 级别内存块的 Cache、申请、释放。
public static void main(String[] args) { PooledByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT; int chunkSize = 1 * 1024 * 1024; ByteBuf byteBuf = allocator.heapBuffer(chunkSize); int chunkSize2 = 9 * 1024; ByteBuf byteBuf2 = allocator.heapBuffer(chunkSize2); byteBuf.release(); byteBuf2.release(); ByteBuf byteBuf3 = allocator.heapBuffer(chunkSize2); byteBuf3.release(); }
原理
由于这里比较复杂,直接看代码可能会云里雾里的,因此这里先介绍一般性原理。
自建内存管理的必要性
Netty 自建内存管理模块是从性能的角度考虑的,参考 https://netty.io/wiki/using-as-a-generic-library.html
1. java.io.ByteBuffer 遵循 Java 的对象申请的约定,即内存申请之后,会将内存块清零后返回给应用层。清零操作一般性的避免了脏内存的使用,然而却消耗了 CPU 和内存带宽,并且网络应用基本会立即重新填充这一块内存,因此基于 Netty 的这种特殊性,是没有必要使用清零后的内存的。
2. java.io.ByteBuffer 的回收是交给 GC 机制的。GC 机制应用于 JVM 堆内存是完全 OK 的,然而不太使用直接内存。直接内存的设计之初的期望就是长时间持有,因此频繁的申请和释放生命周期短的 NIO buffer 的速度是不足够的,并且容易引起内存泄漏。
基于上述两条,我们可以知道 Netty 这种对性能要求比较苛刻的网络框架还是有必要自建内存的。
局部性原理和Cache 机制
局部性原理指的是计算机的同一指令或数据在一段时间内可能会被多次访问。基于这样的前提,我们可以在较低的成本下完成类似较高成本才能完成的事情。对于冯诺依曼体系结构的计算机来讲,从硬盘到内存,内存到 cache,cache 到寄存器,容量越来越小,速度越来越快,这种 Cache 机制就是局部性原理在实践当中的一个反应。Netty 作为一个运行在计算机上的程序,自然也跳脱不出这个一般性的原理。因此 Netty 的实践当中就有了 PooledByteBufAllocator,并且通常我们使用的也是这个内存分配器。
Chunk 级别内存管理机制
从使用者的角度看,Netty 对外提供的内存申请和释放的 API 集中在 Allocator 中。Allocator 当中有多个 Arena,每个 Arena 服务于多个线程,一个线程只使用一个 Arena,这样有助于减少内存分配和释放过程中的锁争用,提高并行化。
Arena
线程通过 Arena 向 JVM 和操作系统申请内存。上面已经讲过了 Cache 的机制,PoolArena 申请内存的时候,小于 huge 的内存是以 PoolChunk 为基本单位申请的,释放的时候,也是先释放到 PoolChunk 当中,如果 PoolChunk 全部释放完毕,才会归还给 JVM 或操作系统。下面我们先看下 PoolArena 当中的数据结构,如下图:

PoolThreadCache


浙公网安备 33010602011771号