heap_base入门到入坟
Zach0ry Lv4

堆管理器ptmalloc

程序运行时堆提供动态分布的内存,从而允许程序申请大小未知的内存,堆由低地址向高地址方向增长,这和栈是相反的(栈是由高地址向低地址方向生长)

每个系统都有自己的堆管理器,不同平台的堆管理器是不一样的,比如Windows、Linux、Mac的处理机制不一样,用户请求堆块和释放堆块的流程也不一样。

堆利用就是针对堆管理器的一种利用思路。它就像是中介,为了避免系统与用户频繁交互(因为和操作系统交互非常耗时),就有了堆管理器。实际上,用户平时申请(malloc)和释放(free)堆块的时候,是和堆管理器直接接触的。

ptmalloc基本功能

主要负责完成用户的两个需求:malloc(申请堆块)和free(释放堆块)

在ptmalloc堆管理器中,内核一般会预先分配一块很大的连续的内存,然后让堆管理器通过某种算法管理这块内存,只有当出现堆空间不足的情况时,ptmalloc堆管理器才会再次与操作系统进行交互,通过系统调用申请内存

释放堆块的时候也是一样,ptmalloc堆管理器需要管理用户释放的堆块,会通过一系列的bin进行管理。bin在ptmalloc中用来保存没有使用的chunk(释放之后的chunk)。根据需求的不同,bin在数据结构上表现为单向链表或者双向循环链表。而且用户请求堆块的时候会优先来自这些释放的堆块

malloc和free

malloc

空间不够 >返回null

参数为0 >返回一个最小的chunk

参数为负>申请有个很大的空间(一般不会成功,会返回null)

free

参数为null> 无影响

参数指向的地方已经被释放时> 除非被禁用(使用 mallopt),否则当释放非常大的内存空间时,将尽可能自动触发将未使用的内存返还给系统的操作,从而减小程序占用。

内存分配背后的系统调用

ptmalloc通过brk/sbrk和mmap/munmap这两组系统调用实现交互

image

brk/sbrk

对于堆的操作,Linux操作系统提供了brk函数,glibc库提供了sbrk函数。我们可以通过增加brk的大小来向操作系统申请内存。

初始时,堆的起始地址start_brk以及堆的当前末尾brk指向同一地址。

不开启ASLR保护时,start_brk和brk会指向data/bss段的结尾。

开启ASLR保护时,start_brk和brk也会指向同一位置,即在data/bss段结尾后的随机偏移处。

现代操作系统每次重启时都会开启ASLR,所以堆的起始地址也是随机的

brk(): brk() 函数接受一个参数,即新的 brk 指针的 绝对地址。

可以将 brk 指针设置到任何你想要的合法位置,通常是向上移动以申请更多内存。

它的返回值为 0 表示成功,-1 表示失败。

这个函数提供的是 设置 功能。

sbrk(): * sbrk() 函数接受一个 增量 参数(increment)。

它将当前的 brk 指针 向前或向后移动 指定的字节数。

它返回移动前的 brk 指针的地址。如果返回 (void*)-1,则表示失败。

这个函数提供的是 增量 功能。

当使用 brk() 或 sbrk() 申请内存时,实际上是移动了数据段的末尾(brk 指针),从而获得更大的堆空间。

mmap/munmap

malloc会使用mmap来映射数据段,内核会为你分配一块物理内存,并将其内容初始化为 0,且mmap 默认创建的是私有映射。在这种模式下,被映射的内存区域属于调用它的进程,即使这个区域是文件映射,该进程对它的修改也不会反映到原文件中,也不会影响到其他同样映射了此文件的进程。

munmap与mmap对应,其作用是释放申请的内存

堆相关的数据结构

malloc_chunk_6

malloc 分配的内存被称为 chunk,它们在被释放后不会立即归还给系统,而是进入以双向链表形式组织的 bin*中。

在malloc_chunk的定义中,共有6个字段,即prev_size、size、fd、bk、fd_nextsize、bk_nextsize。

prev_size和size是两个size_t字段,而fd/bk、fd_nextsize/bk_nextsize都是指针字段,实际上记录了两个双向链表。

image

prev_size

存储前一个 chunk 的大小

在 malloc_chunk结构体中,prev_size 字段位于 size`字段之前

当前一个物理相邻的chunk是空闲的(free):

ptmalloc 库可以通过这个值,快速找到前一个空闲 chunk 的起始位置,从而将两个空闲 chunk 合并成一个更大的空闲 chunk,避免内存碎片

当前一个物理相邻的chunk正在使用(in use):

前一个 chunk 的尾部是用户数据

那么,你当前 chunk 的prev_size 字段,其实位于前一个 chunk 的用户数据区内部。

此时,ptmalloc 知道前一个 chunk 是被占用的,所以它不会去读取 prev_size 这个字段。因此,这个字段的空间可以被前一个 chunk 的用户数据所占用(空间复用)

image

image

size

存储着当前chunk的大小,是2*SIZE_SZ的整数倍

在32位的系统中size是8的整数倍,64位系统中是16的整数倍

用户程序申请的大小都会向上取整,比如在32位程序中,用户申请的大小为0x14,那么向上取整后为0x18。但实际上,在分配的时候,因为空间复用,所以分配0x14字节的chunk的时候,结果会和预想的不一样

把size以二进制写出来会发现后三位都是0,所以将最低的三位用于存储三个标志位,分别是A|M|P,从高位到低位,

其对应的解释如下:

A (NON_MAIN_ARENA):

ptmalloc 支持多线程,每个线程可以有自己的内存池,这被称为 arena,标志位表示是不是主arena。

如果 A 是 1 > 这个 chunk 来自一个非主 arena

如果 A 是 0 > 这个 chunk 来自主 arena

M (IS_MMAPED):

表示当前 chunk 是通过 mmap 分配的,还是通过 brk 从主堆中分配的

M 是 1 > 这个 chunk通过 mmap 系统调用获得的,当它被释放时,ptmalloc 不会把它放入空闲链表(bin),而是直接调用 munmap 将其归还给操作系统

M 是 0 > 这个 chunk是通过 brk() 从主堆分配的 ,当它被释放时,ptmalloc 会将其放入对应的 bin 中

P (PREV_INUSE):

表示前一个物理相邻的 chunk 是否正在使用中

P 是 1 > 说明前一个 chunk 正在被占用,ptmalloc 在释放当前 chunk 时,就不需要检查和合并前一个 chunk。

P 是 0 > 说明前一个 chunk 是空闲的。ptmalloc 在释放当前 chunk 时,会去查看前一个 chunk,并尝试将两个空闲的 chunk 合并成一个更大的块,从而避免内存碎片

eg:

0x71,0x71==0x70|0b001,表示这个chunk的大小为0x70,对应的三个标志位为:A=0,M=0,P=1,即这个chunk属于主线程,不是由mmap系统调用分配,前一个物理相邻的chunk在使用中。

❑0x105,0x105==0x100|0b101,表示这个chunk的大小为0x100,对应的三个标志位为:A=1,M=0,P=1,即这个chunk不属于主线程,由其他线程分配,不是由mmap系统调用分配,前一个物理相邻的chunk在使用中。

想象一下 arena 是一个内存分配的工厂。每个工厂都有自己的原材料(堆空间)和一套管理流程。

主 arena:每个进程都有且只有一个主 arena。它使用进程的主堆(main heap,通过 brk() 扩展的数据段)作为原材料。所有线程都可以从主 arena 申请内存。

非主 arena:在多线程环境中,如果多个线程同时请求内存,为了避免锁竞争,ptmalloc 会为某些线程创建新的 arena。这些新的 arena 不使用主堆,而是通过 mmap() 从操作系统获取独立的堆空间来作为自己的原材料。

让我们用一个简化的流程来描述 chunk 和 arena 的关系:

线程 A 调用 malloc() 请求 100 字节内存。

ptmalloc 检查线程 A 是否有自己的 arena。如果这是第一个线程,它会使用主 arena。

主 arena 检查自己的空闲链表(bin)。如果没有合适的chunk,它会通过 brk() 扩展自己的主堆,然后从新空间中切出 100 字节的chunk,并返回给线程 A。

线程 B 同时调用 malloc() 请求 200 字节内存。

如果此时主 arena 被线程 A 锁住,ptmalloc 会为线程 B 创建一个新的 arena。

这个新 arena 通过 mmap() 获得自己的堆空间。然后从这块新空间中分配一个 200 字节的chunk,并返回给线程 B。

总而言之,arena 就像一个独立的内存管理单元,它拥有和管理自己的堆空间,并负责分配和回收 chunk。chunk 则是 arena 管理下的最小内存单元。

fd/bk(forward/backward)

当chunk处于分配状态(使用状态)的时候,从fd这个字段开始是用户的数据,所以如果用户程序分配的堆块大小是0x20,那么实际的开销是0x20+0x10(64位程序),多余的0x10是glibc为了维护所有的chunk而产生的开销。此时fd/bk不使用,空间被用户数据复用

如果一个chunk处于未使用状态,那么fd/bk就会记录相应的链表信息。当一个 chunk 被释放后,ptmalloc 会将它放入相应的空闲链表bin

fd 指针会指向链表中的下一个空闲 chunk,先进入链表

bk 指针会指向链表中的上一个空闲 chunk,后进入链表

fd_nextsize/bk_nextsize

large_bin中存储着不同大小的chunk

fd_nextsize (forward next size):指向下一个尺寸更大的空闲 chunk

bk_nextsize (backward next size):指向上一个尺寸更小的空闲 chunk

tcatch和bin

tcache 和 bin 都是 Glibc(GNU C Library)中 malloc 实现(称为 ptmalloc2)用于管理空闲内存块的数据结构。当你调用 free() 释放一块内存时,系统不会立即将它归还给操作系统,而是由这些结构暂时保管,以便后续的 malloc() 请求能够快速分配出去

image

Tcache bin

glibc-2.26之后引入的,是优先于fast_bin的chunk缓冲区

  1. 是什么?

    • 它把小尺寸按大小类分成若干个 bin,每个 bin 保存最近 freed 的若干个同 size chunk,malloc 会先从 tcache 拿,free 会先放到 tcache。
    • free(ptr):
      1. 计算 chunk size → 找对应 tcache bin。
      2. 若该 bin 的条目数 < 最大值(7)(随libc版本有差异),将 chunk 放入 tcache(在 payload 开头写上 next 指针,链到当前 head);不会立即写入 fastbin/unsorted。
      3. 若 bin 已满(≥7),则这个 chunk 按传统路径继续(如放到 fastbin / unsorted bin)。
    • malloc(size):
      1. 先看 tcache 对应 bin 是否非空:若非空,从 tcache pop 出第一个 chunk(LIFO),返回给调用者。
      2. 若 tcache 空,继续到 fastbin/smallbin/largebin/unsorted/bin 或调用内核扩展(brk/mmap)等。

    重要:tcache 是LIFO(后进先出)单链表,且存放在 user area(payload),所以写入/读取都是直接对 payload 地址操作

  2. 为什么需要它?

    • 在多线程程序中,如果所有线程都去一个“公共仓库”(全局的 bin)里取放内存,就必须用锁来防止数据混乱。频繁的加锁解锁操作会成为性能瓶颈。
    • tcache 为每个线程提供了一个“私人抽屉”,大部分操作都在这个私人空间里完成,避免了去“公共仓库”竞争,所以速度极快。
  3. 工作原理:

    • 每个线程的 tcache 包含 64 条单链表。
    • 每条链表专门用于存放一种特定大小的内存块(例如,64位中第一条链表放 24 字节的块,第二条放 32 字节的块…以此类推,32位中最小是12递增为8)
    • 每条链表最多缓存 7 个空闲块。
  4. 特点总结:

    • 线程私有:每个线程都有自己的,不共享。
    • 无锁操作:速度快,性能高。
    • 容量小:只有64个大小类,每条链表最多7个块。
    • 优先级最高:malloc/free 会优先操作 tcache。

    glibc是在2.26的版本之后出现的,2.29对它进行了加固


Bin

image

image

(垃圾桶 / 回收站)

  1. 是什么?

    • bin 是 全局共享的 空闲内存块链表。它是堆内存管理的基础结构,所有大小的内存块最终如果没被 tcache 接收,都会由对应的 bin 来管理。你可以把它想象成公共仓库。
  2. 为什么需要它?

    • 它是堆管理器的核心基础设施,用于服务所有大小的内存分配请求,尤其是那些 tcache 处理不了的大块内存,或者当 tcache 已满时。
  3. 工作原理与分类:
    bin 不是一个单一结构,而是一个系列,根据管理策略不同分为四种:

    Fast_bin:(后进先出)

    image

    • 特点:单链表,操作最快。专门用于存放非常小的内存块。
    • 行为:同样大小的块才会放在同一个链表中。free 时会有简单的检查来防止连续的 double free。
    • 不改变使用标志p,也就是说它无法合并(一般不合并)
    • max_fast 是一个全局变量,它定义了最大能够被当作“快速块”处理的内存大小。它的值决定了哪些内存块在释放时会进入分配速度最快的 fastbin 结构

    Unsorted_bin:

    image

    • 特点:双向链表。这是一个“中转站”或“大杂烩”。unsorted bin中的chunk处于乱序的状态,和时间、大小没有固定的关系。所有不在fast bin范围内的堆块释放后,都会先放入unsorted bin中。
    • 来源:一个较大的chunk被分割成两部分后,如果剩下的部分大于MINSIZE(64位系统中为0x20),就会被放到unsorted bin中;释放一个不属于fast bin的chunk,并且该chunk不和top chunk(即最初分配的很大的chunk)紧邻时,该chunk会被首先放到unsorted bin中。
    • 行为:当释放的块较大,或者来自 tcache(当 tcache 满时),会先放到这里。分配器在找不到合适块时,会来“整理”这个bin,把里面的块要么分配出去,要么归并到正确的 smallbin 或 largebin 中。

    Small_bin:(先进先出)

    image

    • 特点:双向链表。存放稍大一些的小内存块,大小范围是[0x20,0x400-0x10]。

    • 每个bin链的头部的fd和arena相连,每个bin链的尾部的bk和arena相连,这里的arena指的是对应线程的arena(malloc_state结构体),如果我们能打印一个在非fast bin链中的fd/bk,就有可能知道libc的基地址。

    • small bin中的每个chunk的大小与其所在的bin的index的关系为:chunk_size=2*SIZE_SZ*index。

      image

    Largebin:

    image

    large bin中一共有63个bin,每个bin中的chunk大小不一致,而是处于一定的区间范围内。这63个bin被分成6组,每一组bin的chunk大小公差一致,其大小和下标的对应关系

    image

    以64位系统的large bin为例进行说明。第一个large bin的起始chunk大小为1024字节,位于第一组,z这个组内有32个人槽,第一个槽可以存储的chunk的大小范围为[1024,1024+64);第二个large bin的起始chunk大小为1024+64,位于第一组,该bin可以存储的chunk的大小范围为[1024+64,1024+64*2)。

    • 特点:双向链表。用于存放大的内存块。
    • 行为:一个链表中存放的是一个范围内的块大小,而不是固定大小。这些块会按大小排序,以便快速找到最合适的块。

tcache>fast_bin>undorted_bin>bin

内存分配的步骤

锁>fast_bin>small_bin

如果都不符合,遍历fast_bin,把相邻的bin进行合并,链接到unsorted_bin中

unsort_bin>申请,切割,<直接放入对应的bin链

还不符合就会到large_bin,然后是top_chunk

判断是不是主分配区去增加top_chunk,是用sbrk,不是用mmap(这个要判断是不是在分配阈值内)

分配阈值会映射一段区域返回,<如果是第一次调用malloc,主分配区域会先分配一块作为heap,不是第一次就brk()增加heap大小,非主分配去就切出一个chunk来返回

首先,系统会获取我们分配区的锁,如果找到没有加所的分配区,就先对改分配区上锁,如果所有都上锁了,就可以开新的分配区,有了自己的分配区后

第 1 步:转换请求大小与对齐

  • 动作:malloc(size) 中的 size 是用户请求的字节数。分配器会将其转换为实际分配的内存块大小。
  • 计算:chunk_size = size + metadata_size + alignment_padding
    • metadata_size:包含存储块大小(size)和标志位(如 P, M, A)的空间,通常为 2 * sizeof(size_t)。
    • alignment_padding:确保最终的内存地址满足对齐要求(例如 64 位系统下 16 字节对齐)。
  • 目的:确保所有分配的内存都正确对齐,并且有空间存放管理信息。

第 2 步:检查 Tcache(线程本地缓存)

  • 条件:如果请求的大小在 tcache 支持的范围内(通常是 64 个大小类,每个最大约 1032 字节)。
  • 动作:
    1. 定位到当前线程的 tcache 结构。
    2. 找到对应大小类的单链表。
    3. 如果链表非空:直接从链表头部取出(unlink)第一个 chunk,将其地址返回给用户。整个过程完全无锁,速度极快。
    4. 如果链表为空:继续后续步骤。但在从后续步骤获取到块之后,会尝试填充 tcache。

第 3 步:检查 Fastbin

  • 条件:如果请求的大小属于 fastbin 范围(小于等于 global_max_fast,默认 128 字节)。
  • 动作:
    1. 获取 arena 的锁(因为 fastbin 是全局的,需要同步)。
    2. 找到对应大小类的 fastbin 单链表(LIFO)。
    3. 如果链表非空:取出表头的 chunk。
    4. Tcache Fill:检查当前线程的 tcache 对应链表是否还有空位(默认最多7个)。如果有,则尝试从 fastbin 中取出多个块(直到取完或 tcache 被填满)放入 tcache,然后返回最初取出的那个块。
    5. 如果链表为空:则继续后续步骤。

第 4 步:检查 Smallbin

  • 条件:如果请求的大小属于 smallbin 范围(小于 1024 字节)。
  • 动作:
    1. 找到对应大小类的 smallbin 双向链表(FIFO,每个bin中的块大小严格一致)。
    2. 如果链表非空:取出链表尾部的一个 chunk。
    3. Tcache Fill:同样,尝试用这个大小的块填充 tcache。
    4. 如果链表为空:则继续后续步骤。

第 5 步:核心循环 - 处理 Unsorted Bin

这是最复杂的一步,是分配器的“垃圾处理厂”和“中转站”。

  • 动作:
    1. 遍历 unsorted bin:它是一个双向链表,存放着最近被释放的、还未分类的 chunk。
    2. 检查每一个 chunk:
      • 精确匹配? 如果遇到一个 chunk 的大小正好等于请求的大小,则立即将其取出并返回。
      • 是 Last Remainder? 如果遇到 last remainder chunk 且它的大小足够大,则对其进行分割:一部分返回给用户,剩余部分成为新的 last remainder(一种优化局部性的策略)。
      • 不匹配则分类:如果当前 chunk 既不匹配也不适合分割,就根据其大小将其从 unsorted bin 中卸下,并链入对应的 smallbin 或 largebin 中。这个过程称为 “unsorted bin 的清理”。
    3. 这个循环会一直进行,直到分配到块,或者 unsorted bin 被清空。

第 6 步:检查 Largebin

  • 条件:如果请求的大小属于 largebin 范围(大于等于 1024 字节)。
  • 动作:此时 unsorted bin 已被清空,分配器开始在对应的 largebin 中查找。
    • largebin 中的 chunk 按大小排序。
    • 分配器使用“最佳匹配”策略,寻找一个大小大于等于请求大小的最小 chunk。
    • 如果找到,就进行分割:一部分返回给用户,剩余部分作为新的空闲块放回 unsorted bin。

第 7 步:使用 Top Chunk(最后的挣扎)

如果以上所有步骤都找不到合适的空闲块,分配器只能求助于最后的家底——Top Chunk。

  • 检查:判断 top chunk 的大小是否足够。

  • 行动:

    • 如果足够:直接从 top chunk 中切割出所需的大小,剩余部分形成新的 top chunk。

    • 如果不足:

      此时就要判断是不是在主分配区了,如果在主分配区就要调用sbrk()去增大top chunk,如果在非住分配区就要调用mmap()来增加top chunk大小或者直接分配,这里好药判断是否在mmap的分配阈值内,大于就会映射一段区域返回给用户。小于就要判断是否是第一次调用malloc,是第一次就先分配一块区域作为我们的heap,如果初始化过了,住分配区就brk()增加heap的大小,非住分配区就早top chunk中切出一个chunk返回给用户

      1. 先尝试合并 fastbin 中的块(通过调用 malloc_consolidate)来扩充 top chunk。
      2. 如果还不够,则调用 sys_brk 或 sys_mmap 向操作系统申请新的内存来扩大 top chunk。
      3. 然后再从扩大后的 top chunk 中进行切割。

image

内存回收的步骤

锁>

1.获取分配区的锁,以此保证线程安全

2.判断传入的指针是否为0,如果为0就直接返回传入。 free() 函数的指针是之前由 malloc()、calloc() 或 realloc() 分配的内存块的起始地址。这个指针至关重要,因为它告诉 free() 函数需要释放哪一块内存。free() 会利用这个地址,以及存储在内存块元数据中的信息,来确定要释放的内存块的大小,然后将其归还给内存分配器(或内核)。

3.不是0的话就判断是否为mmap分配的,是就调用munmap来释放

4.判断大小,小于max_fast,位于heap的顶端就和top chunk合并。不位于heap的顶端就放在fast_bin中,并且不改变p标志位

5.大于max_fast,判断前一个chunk是否空闲,如果是free状态就把他们合并在一起,接着判断是否与top_chunk相邻,相邻就合并

6.如果不与top_chunk合并,就判断下一个chunk是否为空闲的,是就把他们合并放在unsorted bin中,并更新合并后的chunk的大小

7.如果合并后的大小>FASTBIN_CONSOLIDATION_THRESHOLD 的,会遍历fast_bin链,和相邻chunk合并,然后链入unsorted_bin

8.最后判断top_chunk是否在mmap收缩范围内,主函数:会返回top chunk的一部分给系统,但是初始堆大小不会改变,非主分配区:收缩sub_chunk,吧top_chunk还给系统

FASTBIN_CONSOLIDATION_THRESHOLD 是一个在 glibc 内存分配器(ptmalloc)中使用的内部阈值,用于决定何时触发**fastbin 的合并(consolidation)**操作。此时我们的chunk会被遍历并与chunk合并然后链入我们的unsortedbin中,此时fast_bin会空

此时要判断是否超过这个阈值

判断 top chunk 的大小,是否大于 mmap 的收缩阈值(即 M_TRIM_THRESHOLD,通常默认值是 128KB)

主分配区:如果合并后的 top chunk 确实大于这个阈值,glibc 会调用 brk 系统调用,将 top chunk 末尾多余的部分收缩,并将这部分内存归还给操作系统。主分配区最初由 brk 扩展而来,这部分初始内存(通常是 128KB 或更大)是不会被收缩的。只有在程序后续通过 sbrk 进一步扩展的堆内存,才可能被收缩并归还。这是一种优化,因为它避免了频繁地收缩和扩展最初的堆区域

非主分配区:当非主分配区的 top chunk 在释放时变大,并且满足收缩条件(大于 M_TRIM_THRESHOLD),它会将 top chunk 的一部分或全部还给操作系统。如果一个线程释放了所有它自己分配的内存,并且合并后的 top chunk 包含了整个非主分配区的所有内存,那么 glibc 就会调用 munmap 系统调用,将整个 sub-heap(也就是这个线程的堆)归还给操作系统。这使得其他线程或进程可以立即重用这块内存,极大地提高了多线程环境下的内存回收效率。

特殊的chunk

Top Chunk(顶级块 / 荒野块)

Top Chunk 是系统已经分配给堆(通过 brk 或 mmap)但还未被 malloc 分配给用户程序的那部分空间。Top Chunk 是堆中所有已分配和空闲 chunk 的“边界”和“扩展基地”。它位于当前堆区的最高地址处。(低地址)

  • 它是堆上最后一块也是最大的一块空闲 chunk。
  • 它不属于任何 bin(fastbin, smallbin, unsortedbin, largebin)。
  • 它的 size 字段记录了它自身的大小。

内存分配的最后手段:当所有的 bin(包括 unsortedbin)都无法满足一个内存分配请求时,分配器就会从 Top Chunk 中“切割”出一块来满足请求。

  • 如果 Top Chunk 的大小足够:直接从其头部切下所需大小的内存,剩下的部分形成新的、更小的 Top Chunk。
  • 如果 Top Chunk 的大小不足:通过 brk() 或 mmap() 系统调用向操作系统申请,扩大 Top Chunk 的大小,然后再进行切割。

堆的扩展与收缩:Top Chunk 是堆与操作系统交互的接口。堆通过调整 Top Chunk 的大小来动态增长或缩小。

整个 malloc 流程是一个精妙的性能与资源利用率之间的权衡:

  • 常见路径(快):size convert -> tcache -> fastbin -> return
  • 非常见路径(慢但高效):smallbin -> unsorted bin -> largebin -> return
  • 保障路径(非常慢):malloc_consolidate -> grow heap via syscall -> top chunk -> return****

Last Remainder Chunk(最后剩余块)

是 unsorted bin 中的一个特殊角色,它是一种优化策略,用于改善内存局部性。

它是什么?

  • 它是最近一次从一个大 chunk 中分割后剩余下来的那部分。
  • 它被单独记录在 unsorted bin 中,并被打上“特殊”的标记。

它有什么用?

场景:假设程序先申请了一块大内存(A),然后又申请了一小块内存(B),随后释放了 A。此时,A 作为一个大空闲块在 unsorted bin 中。接着,程序又申请一块和 B 大小相同的小内存。

  • 如果没有 Last Remainder:分配器可能会从 unsorted bin 里随便找一块合适的内存进行切割,这可能发生在任何地址。
  • 有了 Last Remainder:分配器会优先从刚才剩下的那个大块(即 Last Remainder Chunk)中进行切割。因为刚刚使用过的 B 很可能还在 CPU 缓存中,紧挨着 B 的 Last Remainder 也在缓存中的概率很高。从这个位置分配新的内存,缓存命中率更高,速度更快。

mmaped chunk

特性 普通堆块 (brk) MMaped Chunk
来源 从 Top Chunk 分割而来 直接由 mmap() 系统调用创建
位置 主堆区内 独立的内存映射区
释放行为 放入各类 bin 中缓存,延迟归还 立即 munmap(),归还操作系统
元数据标志 PREV_INUSE (P) 等 IS_MMAPPED (M)
大小 通常小于 mmap阈值(默认128KB) 通常大于等于 mmap阈值
速度 快(无系统调用) 慢(需要系统调用)
主要影响 可能造成堆碎片 保持堆整洁,但可能有地址空间碎片

16.04>2.23

18.04>2.27

19.04>2.2

20.04>2.30

 请作者喝杯咖啡
 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View