netty池化内存管理
简介
本文主要介绍中netty框架中,使用的池化内存管理算法,通过buddy+slab算法来减少内存使用过程中过多的产生内存内碎片问题。
程序内存空间
对于程序或者进程来说,使用的地址是虚拟地址,虚拟地址空间布局如上图所示,在i386架构中,每个进程的虚拟地址都为4G,数据总线为32位,意味着最大的寻址空间为4G。在进程开启时,虚拟地址空间并不会真正的去映射真实的物理地址。当程序去读写这块数据的时候,OS会处理缺页异常,将真正的去申请物理内存,读写数据,然后挂在页表上。这样,虚拟地址跟物理地址就完成了映射关系。
下图为进程,进程虚拟地址空间,段在OS中的数据结构,分别为task_struct描述一个进程信息,mm_struct描述进程的虚拟地址空间,其中有一个vm_area_struct链表,vm_area_struct为段空间。其中有开始地址以及结束地址。
cpu可以通过CR0,CR4控制寄存器控制是否开启分页,对应控制位如下图所示。
可以看到,cpu支持多级分页,其中包括四级分页。每一页的大小也是可以进行控制的,可以从4kb或者4mb。有些cpu甚至支持每页为4GB。我们日常普遍使用的是每页4kb的配置。
通过页表转化,线性地址便可以转化成物理地址,如下图:
由上图可以看到32位的CR3寄存器保存了页目录地址,cpu的mmu在进行转换的时候,首先找到页目录,然后根据线性地址的前十位定位到页目录项PDE,PDE为20位地址,执行PageTable,然后根据中间十位定位PageTable中的PTE项,PTE为20位,保存了页首地址,再根据12位的偏移量便可以定位物理地址的某个字节。由于CR3控制器用来存储进程的页目录地址,所以在进程切换的时候,需要将CR3数据切换为另外进程的页目录地址。由于CPU中TLB缓存保存了线性地址跟物理地址的映射关系,所以进程切换的时候TLB缓存页会被清除。
java内存
jvm线程启动的时候会初始化堆内存,初始化堆的核心代码在universe.cpp中,universe就是堆的意思。核心代码如下图。
可以看到在linux系统中,是调用了系统调用mmap在c堆中分配一块内存。如下图:
buddy算法
在dubbo框架中,使用netty做网络通讯,其内存分配算法默认就是池化内存分配。
池化内存管理中就有伙伴算法+slab算法。
伙伴算法作为内存管理的常用算法,出现在很多地方,不仅仅是netty的池化内存管理有用到,在linux内核中,在jemalloc内存分配算法中都有用到。其核心就是将一块连续的内存一分为二,分为两块大小相等的两个块。形成了一颗二叉树。如下图所示:
通过PoolChunk维护内存,以及整个buddy系统的元数据。
其中memoryMap数组,以及deptMap数组维护了每一片的使用情况。
1, memoryMap[id] = depth_of_id(deptMap中的标记) => 空闲内存,未被分配
2,memoryMap[id] > depth_of_id =>至少一个子节点被分配了,所以我们不能分配他的内存,但是可能其他的子节点依然可用,依然可以分配内存。
3,memoryMap[id] = maxOrder + 1 = 12 =>节点以及被完全分配,包括所有的子节点也不能被分配,已经被标记为不可使用了
slab算法
通过buddy算法,可以有效的规避内存碎片,但是可以看到buddy算法中最小的页为8kb,假设我们要分配1kb的内存,我们将占用buddy系统中的一个8kb的页,这样就产生了7kb的内存内碎片。所以光有buddy算法是还不够的,还需要slab算法,对8KB的页再进行切割,再次减少内存内碎片。
1,需要分配内存<512b则从tiny内存中进行分配。
2,需要分配内存<8kb则从small内存中分配。
3,需要分配内存<16M 则从伙伴系统中分配。
4,需要内存大于16M,这种hugePage大页内存不归内存池管理。