堆内存的使用在linux开发过程中非常普遍,我们有必要了解相关的内存管理方便我们对内存问题的理解和定位。
堆内存结构层次
linux的堆内存管理分为三个层次,分别为分配区area、堆heap和内存块chunk。
- area:堆内存最上层即为分配区area。分配区area分为主分配区(main area)和线程分配区(thread area)。
- 主进程堆空间对应的分配区即为主分配区main area,每个进程仅有一个主分配区,对应我们通常所说的bss段上面堆空间位置。
- 线程堆空间对应的分配区即为线程分配区thread area。理论上每个线程对应一个线程分配区,但避免资源的浪费,线程分配区也有数量上限值,一般有如下关系:
—32位系统:2 * CPU核数量
—64位系统:8 * CPU核数量
当分配区数量达到上限时,将查找一个已有的满足分配需求的可用分配区,否则会阻塞直至能获取满足要求的分配区。因此,最后可能出现几个线程共用一个分配区的情况。
分配区area对应的数据结构为malloc_state,如下:
- heap:每个分配区下最少有一个堆。若当前的堆分配区空闲空间不够满足分配需求且请求内存大小小于128K时,需要对当前的分配区进行扩展,但对于主分配区和线程分配区来说扩容的方式不一样,具体如下:
分配区(malloc_state) | 堆区数量(heap_info) | 扩容方式 |
---|---|---|
主分配区(main area) | 一个 | 调用brk系统调用扩展堆的上限 |
线程分配区(thread area) | 多个 | 调用mmap分配新的文件映射区域,并将该映射区连接到当前的堆区 |
堆区对应的数据结构heap_info具体如下:
- chunk:每次glibc分配的内存块称为chunk。malloc_chunk的数据结构如下:
malloc_chunk数据结构的使用分析可参考:https://blog.csdn.net/sy4331/article/details/120470932
内存管理结构如下:
main area/thread area数据结构空间分布:
空闲内存管理
我们通过free释放的内存一般不会立马释放给内核,而是本地缓存起来,这样下次再申请相同大小内存时可以直接分配,不用再陷入内核,这大大提高了内存申请、释放效率。
为了进一步提高空闲内存的管理效率,glibc将空闲内存分为top chunk和bin chunk。
- top chunk:指堆顶(内存的最高地址)的那一片连续内存。该内存主要用于分配大内存需求,当空闲的内存块都不满足分配需求时,可扩展top chunk。
- bin:已分配然后被回收的内存,是分散在已使用内存中的内存碎片。bin是包括一类具有相同特点的内存chunk块集合。
空闲内存分类如上图所示。bin中根据管理的内存大小及用途,又分为fast bins、unsorted bin、small bins和large bins。
fast bins
fast bins具有最快的申请和释放速度,其对应分配区area数据结构malloc_state中fastbinsY数组。该数组为结构体指针类型,其实为一个个单向链表,每一个数组成员指向链表头。
malloc_state中的fast bins:
特点:
- 每一个单向链表指向的内存chunk大小均相等,即同一个fast bin下的内存块大小相等。
- 相邻的fast bin的内存块chunk大小相差8字节。
- fast bins可存放16、24、32、40、48、56、64、72、80字节大小内存块(包括malloc_chunk的头结构)。
- 释放后的内存块chunk的各类标志位,如PRE_INUSE等均不会改变,相邻的chunk也不会合并。
unsorted bin
当内存块chunk被释放后且大小不在fast bins范围内,则该内存块将被保存在unsorted bin中。unsorted bin中的内存块没有大小限制,且是无序的。unsorted bin采用的是一个单链表连接所有的内存,对应malloc_state中的bins中的bins[1]
small bins
small bins中保存的是大小小于512字节的空闲内存块chunk,其具体放在malloc_state中的bins结构体指针数组中的bins[2]-bins[63]。
特点:
- 每一个大小内存的chunk均采用双向链表表示。如bins[2]、bins[3]分别指向16字节大小chunk bins链表的头部和尾部。
- 每一个双向链表指向的内存chunk大小均相等,即同一个small bin下的内存块大小相等。
- 相邻的fast bin的内存块chunk大小相差8字节。
- fast bins可存放16、24、32、40、48、…、504字节大小内存块(包括malloc_chunk的头结构)。
从上面特点可知,fast bins和small bins属性较为相似,fast bins可以看做small bins的一个高速缓存,small bins保存的内存块大小范围更广。
large bins
large bins中保存的是大小大于等于512字节的内存块,对应保存在malloc_state中结构体指针数组bins中的bins[64]-bins[126]。
特点:
- 每一类内存的bins均采用双向链表表示。如bins[64]、bins[65]分别指向512字节大小chunk bins链表的头部和尾部。
- 每一个双向链表指向的内存chunk大小均不相等,从链表头部向尾部依次递减。
- 相邻的large bin的内存块chunk大小相差不相等,且bins数组下标越大(即bins内存越大),相邻large bin的内存块chunk相差越来越大。
- fast bins保存大于等于512字节大小内存块(包括malloc_chunk的头结构)。
上述几类空闲内存bins特点总结如下;
内存申请流程
内存分配时,查找空闲内存的优先级从高到低为fast bins、small bins、unsorted bins、large bins和top chunk,查找时从高优先级到低优先级一次查找满足需求的内存,直至找到为止。
查找空闲内存优先级顺序:
内存申请的具体流程如下:
- 首先将用户申请的内存长度转化为需要分配的内存块chunk长度chunk_size,这里面需要将用户请求长度加上malloc_chunk的前两个成员uchunk_prev_size和mchunk_size所占用空间(32位系统下占用8字节)。此外,还需要考虑内存对齐问题。对于32位系统,8字节对齐;64位系统,16字节对齐。这里面还涉及最小分配长度问题。32位系统最小分配长度为16字节;64位系统最小分配长度为32字节。当我们在32位系统下调用malloc(0),glibc也会分配16字节内存。
系统位数 | 内存对齐长度 | 最小分配长度 |
---|---|---|
32位 | 8字节 | 16字节 |
64位 | 16字节 | 24字节 |
- 判断需要分配的内存块长度chunk_size是否小于等于fast bins管理内存的最大长度(初始时fast bins管理内存最大长度为64字节,后面为80字节)。当在范围内,则在fast bins里面查找满足需求的内存。若找到,则返回用户内存,结束;否则转到步骤3。
- 判断chunk_size是否在small bins范围内(<512字节)。若在范围内,则查找small bins。若找到满足需求内存,则返回用户内存,结束;否则跳转到步骤4.
- 遍历fast bins,合并相邻内存(保证虚拟地址连续)。将合并后的内存块chunk从fast bins中剥离并加入到unsorted bins。
- 遍历查找unsorted bins。若当前内存块不满足用户需求,则将该内存块根据其大小放到small bins或者large bins;若找到,则返回用户内存,结束。重复步骤5,直至找到满足需求内存或者遍历完也未找到,则跳转到步骤6。
- 遍历large bins。根据“small first,best fit”原则,找到最符合需求的内存块。当找到该内存块,若该内存块可以分割(剩余内存块不小于最小内存块大小),则分割该内存块,将一部分返回给用户,剩余部分放到unsorted bins中;若不能分割,则直接将该内存块返回给用户。若遍历完large bins也没有找到,则跳转到步骤7。
- 经过上述步骤后还没有找到满足需求的内存块,则表明fast bins、small bins、large bins均没有符合要求内存块,此时只能查看top chunk了。若top chunk当前大小能满足要求,则分割top chunk,将低地址返回给用户,高地址作为新的top chunk;否则,跳转到步骤8。
- 判断chunk_size是否已超过mmap分配阈值(128k)。若大于mmap分配阈值,则直接调用mmap分配需求内存空间。否则只能扩展堆区从而增大top chunk了,跳转到步骤9。
- 扩展堆区对于main area和thread area不同。若为main area,则通过调用sbrk系统调用向内核申请扩展堆顶以扩大堆空间;若为thread area,则通过mmap系统调用申请新的内存映射区,并将新的内存映射区添加到当前的分配区,同时,调整top chunk指向新分配的内存映射区,同时将老的top chunk改为普通的free chunk从而扩展堆空间 。通过上面操作,top chunk肯定能满足用户分配需求了,则跳转到步骤7,再次通过top chunk分配满足用户需求的内存块。
glibc内存申请流程图如下:
内存释放流程
内存释放流程相对简单,具体流程如下:
- 根据用户释放的内存地址获取对应的内存块chunk大小。
- 释放的内存块大小是否在fast bins范围内。若在,则将释放的内存放到fast bins中,结束;否则,跳转到步骤3。
- 判断释放的内存是否通过mmap申请的。若是,则直接通过unmmap释放该内存,结束;否则,跳转到步骤4。
- 判断释放内存块的前一个内存块是否为空闲内存(连续地址的低地址侧)。若是,则合并该空闲内存。
- 判断释放内存块的后一个内存块是否为空闲内存(连续地址的高地址侧)。若不是,则直接将内存块放到unsorted bin,结束;否则进一步看该内存块是否为top chunk。若为top chunk,则将待释放内存块合并到top chunk,并跳转到步骤6;否则,合并后一个空闲内存块,并将合并后的内存块放到unsorted bin中,结束。
- 判断当前分配区area是否为main area。若是,则跳转到步骤7;否则,跳转到步骤8。
- 判断当前的top chunk的大小是否超过了内存回收阈值(128k)。若是,则通过brk系统调用释放top chunk一部分内存给内核;否则,结束。
- 判断当前的top chunk所在的堆区内存(即heap_info数据结构管理对应的内存)是否已经全部释放了。若是,则调用unmmap释放该整个堆区(heap_info),将该部分内存返回给内核;否则,结束。
glibc内存释放流程图如下:
通过上述分析可知,我们通常调用free接口释放的内存最终有以下几个去处:
1. fast bins
2. unsorted bin
3. top chunk
4. 通过unmmap释放,将内存返回给内核。
注意,在释放内存时,若该内存最终是放到了fast bins中,为了提高下次分配效率,相邻空闲内存块不会合并,且该内存块和相邻的后一个内存块的头部结构malloc_chunk中的P(PREV_INUSE)、M(IS_MMAPPED)、N(NON_MAIN_AREA)均不会改变,维持原状。但若放在其他处,如unsorted bin、top chunk中,该内存块和相邻的后一个内存块对应的malloc_chunk中的P、M、N标志位均会做相应调整,特别是相邻的后一个内存块的P(PREV_INUSE)标志位会置为0,表示刚刚释放的内存块现处于空闲状态。
参考:https://blog.csdn.net/m0_37765662/article/details/119078410
https://blog.csdn.net/maokelong95/article/details/51989081?spm=1001.2014.3001.5501
https://blog.csdn.net/maokelong95/article/details/52006379?spm=1001.2014.3001.5501