glic内存管理ptmalloc之原理 概述

3.概述

3.1内存管理一般性描述

当不知道程序的每个部分将需要多少内存时,系统内存空间有限,而内存需求又是变化的,这时就需要内存管理程序来负责分配和回收内存。程序的动态性越强,内存管理就越重要,内存分配程序的选择也就更重要。

内存管理的方法

3.3.1.C 风格的内存管理程序

C 风格的内存管理程序主要实现 malloc()和 free()函数。内存管理程序主要通过调用 brk()或者 mmap()进程添加额外的虚拟内存。Doug  Lea  Malloc,ptmalloc,BSD  malloc,Hoard,TCMalloc 都属于这一类内存管理程序

基于 malloc()的内存管理器仍然有很多缺点,不管使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc()来管理内存可能会非常令人失望。

如果有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理, 但是对于生存期超出该范围的内存来说,管理

内存则困难得多。因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。

 

2.池式内存管理

内存池是一种半内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存

例如,很多网络服务器进程都会分配很多针对每个连接的内存——内存的最大生存期限为当前连接的存在期。

Apache  使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。

使用池式内存分配的优点如下所示:

l应用程序可以简单地管理内存。

l内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1)时间内完成,释放内存池所需时间也差不多(实际上是 O(n)时间,不过在大部分情况

下会除以一个大的因数,使其变成 O(1))。

l可以预先分配错误处理池(Error-handling  pools),以便程序在常规内存被耗尽时仍可以恢复。

l有非常易于使用的标准实现。池式内存的缺点是:

l内存池只适用于操作可以分阶段的程序。

l内存池通常不能与第三方库很好地合作。

l如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。

您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。

3.引用计数

在引用计数中,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,是在告诉数据结构,它正在被存储在多少个位置上。然后,当进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。

4.垃圾收集

垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。常,它们以程序所知的可用的一组“基本”数据——栈数据、全局变量、寄存器——作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。

垃圾收集的一些优点:

l永远不必担心内存的双重释放或者对象的生命周期。

l使用某些收集器,您可以使用与常规分配相同的 API。其缺点包括:

l使用大部分收集器时,您都无法干涉何时释放内存。

l在多数情况下,垃圾收集比其他形式的内存管理更慢。

l垃圾收集错误引发的缺陷难于调试。

l如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。

3.2Ptmalloc 内存管理概述

3.2.1简介

Linux 中 malloc 的早期版本是由 Doug Lea 实现的,它有一个重要问题就是在并行处理时多个线程共享进程的内存空间,各线程可能并发请求内存,在这种情况下应该如何保证分配和回收的正确和高效。Wolfram Gloger 在 Doug Lea 的基础上改进使得 Glibc 的 malloc 可以支持多线程——ptmalloc,在glibc-2.3.x.中已经集成了ptmalloc2,这就是我们平时使用的malloc,目前 ptmalloc 的最新版本 ptmalloc3。ptmalloc2 的性能略微比 ptmalloc3 要高一点点。ptmalloc 实现了 malloc(),free()以及一组其它的函数. 以提供动态内存管理的支持。分配器处在用户程序和内核之间,它响应用户的分配请求,向操作系统申请内存,然后将其返回给用户程序,为了保持高效的分配,分配器一般都会预先分配一块大于用户请求的内存, 并通过某种算法管理这块内存。来满足用户的内存分配要求,用户释放掉的内存也并不是立即就返回给操作系统,相反,分配器会管理这些被释放掉的空闲空间,以应对用户以后的内存分配要求。也就是说,分配器不但要管理已分配的内存块,还需要管理空闲的内存块,当响应用户分配要求时,分配器会首先在空闲空间中寻找一块合适的内存给用户,在空闲空间中找不到的情况下才分配一块新的内存。为实现一个高效的分配器,需要考虑很多的因素。比如,分配器本身管理内存块所占用的内存空间必须很小,分配算法必须要足够的快。

3.2.3 内存管理数据结构概述

Glibc 的 malloc 可以支持多线程,由只有一个主分配区(main arena)增加了非主分配区(non main arena)支持,主分配区与非主分配区用环形链表进行管理。每一个分配区利用互斥锁(mutex)使线程对于该分配区的访问互斥。

每个进程只有一个主分配区,但可能存在多个非主分配区,ptmalloc 根据系统对分配区的争用情况动态增加非主分配区的数量,分配区的数量一旦增加,就不会再减少了。

主分配区可以访问进程的 heap 区域和 mmap 映射区域,也就是说主分配区可以使用 sbrk 和 mmap向操作系统申请虚拟内存。

非主分配区只能访问进程的 mmap 映射区域,非主分配区每次使用 mmap()向操作系统“批发”HEAP_MAX_SIZE(32 位系统上默认为 1MB,64 位系统默认为 64MB)大小的虚拟内存,当用户向非主分配区请求分配内存时再切割成小块“零售”出去,毕竟系统调用是相对低效的,直接从用户空间分配内存快多了。所以 ptmalloc 在必要的情况下才会调用 mmap()函数向操作系统申请虚拟内存。

当某一线程需要调用 malloc()分配内存空间时,该线程先查看线程私有变量中是否已经存在一个分配区,如果存在,尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,如果失败,该线程搜索循环链表试图获得一个没有加锁的分配区。如果所有的分配区都已经加锁,那么 malloc()会开辟一个新的分配区,把该分配区加入到全局分配区循环链表并加锁,然后使用该分配区进行分配内存操作。在释放操作中,线程同样试图获得待释放内存块所在分配区的锁,如果该分配区正在被别的线程使用,则需要等待直到其他线程释放该分配区的互斥锁之后才可以进行释放操作。

申请小块内存时会产生很多内存碎片,ptmalloc 在整理时也需要对分配区做加锁操作。

每个加锁操作大概需要 5~10 个 cpu 指令,而且程序线程很多的情况下,锁等待的时间就会延长,导致 malloc 性能下降。一次加锁操作需要消耗 100ns 左右,正是锁的缘故,导致 ptmalloc在多线程竞争情况下性能远远落后于 tcmalloc最新版的 ptmalloc 对锁进行了优化,加入了PER_THREAD 和 ATOMIC_FASTBINS 优化,但默认编译不会启用该优化,这两个对锁的优化应该能够提升多线程内存的分配的效率。

3.2.3.2 chunk 的组织

不管内存是在哪里被分配的,用什么方法分配,用户请求分配的空间在 ptmalloc 中都使用一个 chunk 来表示。用户调用 free()函数释放掉的内存也并不是立即就归还给操作系统,相反,它们也会被表示为一个 chunk,ptmalloc 使用特定的数据结构来管理这些空闲的 chunk。

1.Chunk 格式

ptmalloc 在给用户分配的空间的前后加上了一些控制信息,用这样的方法来记录分配的信息,以便完成分配和释放工作。一个使用中的 chunk(使用中,就是指还没有被 free 掉)

空闲 chunk 在内存中的结构

  1. chunk 中的空间复用????

为了使得 chunk 所占用的空间最小,ptmalloc 使用了空间复用,一个 chunk 或者正在

被使用,或者已经被 free 掉,所以 chunk 的中的一些域可以在使用状态和空闲状态表示不同的意义,来达到空间复用的效果。

3.2.3.3 空闲 chunk 容器

用户 free 掉的内存并不是都会马上归还给系统,ptmalloc 会统一管理 heap 和 mmap 映射区域中的空闲的 chunk,当用户进行下一次分配请求时,ptmalloc 会首先试图在空闲的chunk 中挑选一块给用户,这样就避免了频繁的系统调用,降低了内存分配的开销。 ptmalloc将相似大小的 chunk 用双向链表链接起来,这样的一个链表被称为一个 bin。Ptmalloc 一共维护了 128 个 bin,并使用一个数组(bins)来存储这些 bin(如下图所示)。

bins是空闲堆中的基本数据结构,他们被用来保存空闲堆块。

Small bin:数组中从 2 开始编号的前 64 个 bin 称为 small bins,同一个small bin中的chunk具有相同的大小。两个相邻的small bin中的chunk大小相差8bytes。small bins 中的 chunk 按照最近使用顺序进行排列,最后释放的 chunk 被链接到链表的头部,而申请 chunk 是从链表尾部开始,这样,每一个 chunk 都有相同的机会被 ptmalloc 选中.

Large binsSmall bins 后面的 bin 被称作 large bins。large bins 中的每一个 bin 分别包含了一个给定范围内的 chunk,其中的 chunk 按大小序排列相同大小的 chunk 同样按照最近使用顺序排列。ptmalloc 使用“smallest-first,best-fit”原则在空闲 large bins 中查找合适的 chunk。

Fast bins:引入了 fast bins,不大于 max_fast (默认值为 64B)的 chunk 被释放后,首先会被放到 fast bins 中,fast bins 中的 chunk 并不改变它的使用标志 P。这样也就无法将它们合并,当需要给用户分配的 chunk 小于或等于 max_fast 时,ptmalloc 首先会在 fast bins 中查找相应的空闲块,然后才会去查找bins中的空闲chunk。【LIFO的栈,使用单向链表实现。】

Unsorted Bin:unsorted bin 的队列使用 bins 数组的第一个,如果被用户释放的 chunk 大于 max_fast,或者 fast bins 中的空闲 chunk 合并后,这些 chunk 首先会被放到 unsorted bin 队列中,在进行 malloc 操作的时候,如果在 fast bins 中没有找到合适的 chunk,则 ptmalloc 会先在 unsorted bin 中查找合适的空闲 chunk,然后才查找 bins。如果 unsorted bin 不能满足分配要求。malloc便会将 unsorted bin 中的 chunk 加入 bins 中。然后再从 bins 中继续进行查找和分配过程。unsorted bin 可以看做是 bins 的一个缓冲区,并不是所有的 chunk 都按照上面的方式来组织,实际上,有三种例外情况。Top chunk,mmaped chunk 和 last remainder.

  1. Top chunk:因为内存是按地址从低向高进行分配的,在空闲内存的最高处,必然存在着一块空闲 chunk,叫做 top chunk.

对于非主分配区预先从 mmap 区域分配一块较大的空闲内存模拟 sub-heap,通过管理 sub-heap 来响应用户的需求,当 bins 和 fast bins 都不能满足分配需要的时候,ptmalloc 会设法在 top chunk 中分出一块内存给用户,如果 top chunk 本身不够大,分配程序会重新分配一个 sub-heap,并将 top chunk 迁移到新的 sub-heap 上,新的 sub-heap与已有的 sub-heap 用单向链表连接起来,然后在新的 top chunk 上分配所需的内存以满足分配。top chunk 在分配时总是在 fast bins 和 bins 之后被考虑。。Top chunk 的大小是随着分配和回收不停变换的,如果从 top chunk 分配内存会导致 top chunk 减小,如果回收的 chunk 恰好与 top chunk 相邻,那么这两个 chunk 就会合并成新的 top chunk,从而使 top chunk 变大。如果在 free 时回收的内存大于某个阈值,并且 top chunk 的大小也超过了收缩阈值,ptmalloc会收缩 sub-heap,如果 top-chunk 包含了整个 sub-heap,ptmalloc 会调用 munmap 把整个sub-heap 的内存返回给操作系统。

由于主分配区是唯一能够映射进程 heap 区域的分配区,它可以通过 sbrk()来增大或是收缩进程 heap 的大小ptmalloc 在开始时会预先分配一块较大的空闲内存(也就是所谓的 heap),主分配区的 top chunk 在第一次调用 malloc 时会分配一块(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap,用户从 top chunk 分配内存时,可以直接取出一块内存给用户。在回收内存时,回收的内存恰好与 top chunk 相邻则合并成新的 top chunk,当该次回收的空闲内存大小达到某个阈值,并且 top chunk 的大小也超过了收缩阈值,会执行内存收缩,减小 top chunk 的大小,但至少要保留一个页大小的空闲内存,从而把内存归还给操作系统。如果向主分配区的 top chunk 申请内存,而 top chunk 中没有空闲内存,ptmalloc会调用 sbrk()将的进程 heap 的边界 brk 上移,然后修改 top chunk 的大小。

  1. Mmaped chunk:当需要分配的 chunk 足够大,而且 fast bins 和 bins 都不能满足要求,甚至 top chunk 本身也不能满足分配需求时,ptmalloc 会使用 mmap 来直接使用内存映射来将页映射到进程空间。这样分配的 chunk 在被 free 时将直接解除映射,于是就将内存归还给了操作系统,再次对这样的内存区的引用将导致 segmentation fault 错误。

3.2.3.4 sbrk 与 mmap

.bss 段之上的这块分配给用户程序的空间被称为 heap (堆)。start_brk 指向 heap 的开始,而 brk 指向 heap 的顶部。可以使用系统调用 brk()和 sbrk()来增加标识 heap 顶部的 brk 值,从而线性的增加分配给用户的 heap 空间。在使 malloc 之前,brk的值等于start_brk,也就是说heap大小为0。ptmalloc在开始时,若请求的空间小于 mmap分配阈值(mmap threshold,默认值为 128KB)主分配区会调用 sbrk()增加一块大小为 (128 KB + chunk_size) align 4KB 的空间作为 heap。非主分配区会调用 mmap 映射一块大小为HEAP_MAX_SIZE(32 位系统上默认为 1MB,64 位系统上默认为 64MB)的空间作为 sub-heap

当用户请求内存分配时,首先会在这个区域内找一块合适的 chunk 给用户。当用户释放了 heap 中的 chunk 时,ptmalloc 又会使用 fastbins 和 bins 来组织空闲 chunk。若需要分配的 chunk 大小小于 mmap分配阈值,而 heap 空间又不够,则此时主分配区会通过 sbrk()调用来增加 heap 大小,非主分配区会调用 mmap 映射一块新的 sub-heap,也就是增加 top chunk 的大小,每次 heap 增加的值都会对齐到 4KB.

当用户的请求超过 mmap 分配阈值,并且主分配区使用 sbrk()分配失败的时候,或是非主分配区在 top chunk 中不能分配到需要的内存时ptmalloc 会尝试使用 mmap()直接映射一块内存到进程内存空间。使用 mmap()直接映射的 chunk 在释放时直接解除映射,而不再属于进程的内存空间。任何对该内存的访问都会产生段错误。

内存分配概述:

1. 分配算法概述,以 32 系统为例,64 位系统类似。

l 小于等于 64 字节:用 pool 算法分配。

l 64 到 512 字节之间:在最佳匹配算法分配和 pool 算法分配中取一种合适的。

l 大于等于 512 字节:用最佳匹配算法分配。

l 大于等于 mmap 分配阈值(默认值 128KB):根据设置的 mmap 的分配策略进行分配,

如果没有开启 mmap 分配阈值的动态调整机制,大于等于 128KB 就直接调用 mmap分配。否则,大于等于 mmap 分配阈值时才直接调用 mmap()分配。

ptmalloc 的响应用户内存分配要求的具体步骤为:

1) 获取分配区的锁,为了防止多个线程同时访问同一个分配区,在进行分配之前需要

取得分配区域的锁。线程先查看线程私有实例中是否已经存在一个分配区,如果存

在尝试对该分配区加锁,如果加锁成功,使用该分配区分配内存,否则,该线程搜

索分配区循环链表试试图获得一个空闲(没有加锁)的分配区。如果所有的分配区都

已经加锁,那么 ptmalloc 会开辟一个新的分配区,把该分配区加入到全局分配区循

环链表和线程的私有实例中并加锁,然后使用该分配区进行分配操作。开辟出来的

新分配区一定为非主分配区,因为主分配区是从父进程那里继承来的。开辟非主分

配区时会调

  1. 将用户的请求大小转换为实际需要分配的 chunk 空间大小
  2. 判断所需分配chunk的大小是否满足chunk_size <= max_fast (max_fast 默认为 64B),如果是的话首先尝试在 fast bins 中取一个所需大小的 chunk 分配给用户。如果可以找到,则分配结束否则转第4步,
  3. 判断所需大小是否处在 small bins 中,即判断 chunk_size < 512B 是否成立。如果chunk 大小处在 small bins 中。 根据所需分配的 chunk 的大小,找到具体所在的某个 small bin,从该 bin 的尾部摘取一个恰好满足大小的 chunk。若成功,则分配结束。否则转第5步,
  4. 这说明需要分配的是一块大的内存,或者 small bins 中找不到合适的chunk。于是,ptmalloc 首先会遍历 fast bins 中的 chunk,将相邻的 chunk 进行合并,并链接到 unsorted bin 中,然后遍历 unsorted bin 中的 chunk,如果 unsorted bin 只有一个 chun并且这个 chunk 在上次分配时被使用过,并且所需分配的 chunk 大小属于 small bins,并且 chunk 的大小大于等于需要分配的大小,这种情况下就直接将该 chunk 进行切割,分配结束。否则转入第6步
  5. 这说明需要分配的是一块大的内存,或者 small bins 和 unsorted bin 中都找不到合适的 chunk,并且 fast bins 和 unsorted bin 中所有的 chunk 都清除干净了。根据 chunk 的空间大小将其放入 small bins 或是 large bins 中,遍历完成后,从 large bins 中按照“smallest-first,best-fit”原则,找一个合适的 chunk,从中划分一块所需大小的 chunk,并将剩下的部分链接回到 bins 中。若操作成功,则分配结束否则转入第7步,
  6. 如果搜索 fast bins 和 bins 都没有找到合适的 chunk,那么就需要操作 top chunk 来进行分配了。判断 top chunk 大小是否满足所需 chunk 的大小,如果是,则从 top chunk 中分出一块来。转第9步,否则转入第8步
  7. 这说明 top chunk 也不能满足分配要求,所以,于是就有了两个选择: 如果是主分配区,调用 sbrk(),增加 top chunk 大小;如果是非主分配区,调用 mmap来分配一个新的 sub-heap,增加 top chunk 大小;或者使用 mmap()来直接分配。我们判断所需分配的 chunk大小是否大于等于 mmap 分配阈值,如果是的话,转第9步,否则转第10步增加 top chunk 的大小。
  8. 使用mmap 系统调用为程序的内存空间映射一块 chunk_size align 4kB 大小的空间。然后将内存指针返回给用户。
  9. 判断是否为第一次调用 malloc,若是主分配区,则需要进行一次初始化工作,分配一块大小为(chunk_size + 128KB) align 4KB 大小的空间作为初始的 heap。若已经初始化过了,主分配区则调用 sbrk()增加 heap 空间,分主分配区则在 top chunk 中切割出一个 chunk,使之满足分配需求,并将内存指针返回给用户

总结:根据用户请求分配的内存的大小,ptmalloc 有可能会在两个地方为用户分配内存空间。在第一次分配内存时,一般情况下只存在一个主分配区,但也有可能从父进程那里继承来了多个非主分配区,在这里主要讨论主分配区的情况,brk 值等于start_brk,所以实际上 heap 大小为 0,top chunk 大小也也是 0。这时,如果不增加 heap大小,就不能满足任何分配要求。所以,若用户的请求的内存大小小于 mmap 分配阈值,则 ptmalloc 会初始 heap。然后在 heap 中分配空间给用户,以后的分配就基于这个 heap进行。若第一次用户的请求就大于 mmap 分配阈值,则 ptmalloc 直接使用 mmap()分配一块内存给用户,而 heap 也就没有被初始化,直到用户第一次请求小于 mmap 分配阈值的内存分配。第一次以后的分配就比较复杂了,简单说来,ptmalloc 首先会查找 fast bins,如果不能找到匹配的 chunk,则查找 small bins。若还是不行,合并 fast bins,把 chunk加入 unsorted bin,在 unsorted bin 中查找,若还是不行,把 unsorted bin 中的 chunk 全加入 large bins 中,并查找 large bins。在 fast bins 和 small bins 中的查找都需要精确匹配,而在 large bins 中查找时,则遵循“smallest-firstt,best-fit”的原则,不需要精确匹配。若以上方法都失败了,则 ptmalloc 会考虑使用 top chunk。若 top chunk 也不能满足分配要求,而且所需 chunk 大小大于 mmap 分配阈值,则使用 mmap 进行分配。否则top chunk 也不能满足分配要求,而且所需 chunk 大小不大于 mmap 分配阈值则调用 sbrk()将的进程 heap 的边界 brk 上移来先增加heap,进而增大 top chunk;非主分区通过mmap分配新的sub-heap,即增加topchunk。以满足分配要求

内存回收概述:

free() 函数接受一个指向分配区域的指针作为参数,释放该指针所指向的 chunk。而具

体的释放方法则看该 chunk 所处的位置该 chunk 的大小

free()函数的工作步骤如下:

1) free()函数同样首先需要获取分配区的锁,来保证线程安全。

2) 判断传入的指针是否为 0,如果为 0,则什么都不做,直接 return否则转下一步

3) 判断所需释放的 chunk 是否为 mmaped chunk,如果是,则调用 munmap()释放

mmaped chunk,解除内存空间映射,该该空间不再有效。如果开启了 mmap 分配

阈值的动态调整机制,并且当前回收的 chunk 大小大于 mmap 分配阈值,将 mmap

分配阈值设置为该 chunk 的大小,将 mmap 收缩阈值设定为 mmap 分配阈值的 2

倍,释放完成否则转下一步

4) 判断 chunk 的大小和所处的位置,若 chunk_size <= max_fast(64B),并且 chunk 并不位于heap 的顶部,也就是说并不与 top chunk 相邻, 将 chunk 放到 fast bins 中,chunk 放入到 fast bins 中时,并不修改该 chunk 使用状态位 P。也不与相邻的 chunk 进行合并。只是放进去,如此而已。这一步做完之后释放便结束了,程序从 free()函数中返回。否则转下一步,

5)判断前一个 chunk 是否处在使用中,如果前一个块也是空闲块,则合并。 判断当前释放 chunk 的下一个块是否为 top chunk,如果是, 说明释放了一个与 top chunk 相邻的 chunk。则无论它有多大,都将它与 top chunk 合并,并更新 top chunk 的大小等信息,并转第7步。如果不是topchunk否则转下一步,

6)判断下一个 chunk 是否处在使用中,如果下一个 chunk 也是空闲的,则合并,并将合并后的 chunk 放到 unsorted bin 中。注意,这里在合并的过程中,要更新 chunk的大小,以反映合并后的 chunk 的大小。

7)判断合并后的 chunk(5或6) 的大小是否大于 FASTBIN_CONSOLIDATION_THRESHOLD(默认64KB),如果是的话,则会触发进行 fast bins 的合并操作,fast bins 中的 chunk 将被遍历,并与相邻的空闲 chunk 进行合并,合并后的 chunk 会被放到 unsorted bin 中。fast bins 将变为空。

8)再判断 top chunk 的大小是否大于 mmap 收缩阈值(默认为 128KB),【内存收缩】如果是的话,对于主分配区,则会试图归还 top chunk 中的一部分给操作系统。但是最先分配的128KB 空间是不会归还的,ptmalloc 会一直管理这部分内存,用于响应用户的分配请求;如果为非主分配区,会进行 sub-heap 收缩,将 top chunk 的一部分返回给操作系统,如果 top chunk 为整个 sub-heap,会把整个 sub-heap 还回给操作系统。 释放结束!

 

3.2.6 配置选项概述

1. M_MXFAST

M_MXFAST 用于设置 fast bins 中保存的 chunk 的最大大小,默认值为 64B,fast bins 中保存的 chunk 在一段时间内不会被合并,分配小对象时可以首先查找 fast bins,如果 fast bins找到了所需大小的 chunk,就直接返回该 chunk,大大提高小对象的分配速度,但这个值设置得过大,会导致大量内存碎片,并且会导致 ptmalloc 缓存了大量空闲内存,去不能归还给操作系统,导致内存暴增

M_MXFAST 的最大值为 80B,不能设置比 80B 更大的值,因为设置为更大的值并不能提高分配的速度。Fast bins 是为需要分配许多小对象的程序设计的,

如果设置该选项为 0,就会不使用 fast bins。

2. M_TRIM_THRESHOLD

M_TRIM_THRESHOLD 用于设置 mmap 收缩阈值,默认值为 128KB。自动收缩只会在 free时才发生,如果当前 free 的 chunk 大小加上前后能合并 chunk 的大小大于 64KB,并且 top chunk 的大小达到 mmap 收缩阈值,对于主分配区,调用 malloc_trim()返回一部分内存给操作系统,对于非主分配区,调用 heap_trim()返回一部分内存给操作系统,

  1. M_MMAP_THRESHOLD

M_MMAP_THRESHOLD 用于设置 mmap 分配阈值,默认值为 128KB,ptmalloc 默认开启动态调整 mmap 分配阈值和 mmap 收缩阈值。

当用户需要分配的内存大于mmap分配阈值,ptmalloc的malloc()函数其实相当于mmap()的简单封装,free 函数相当于 munmap()的简单封装。相当于直接通过系统调用分配内存,

回收的内存就直接返回给操作系统了。因为这些大块内存不能被 ptmalloc 缓存管理,不能重用,所以 ptmalloc 也只有在万不得已的情况下才使用该方式分配内存。

但mmap分配也由于好处:

1.Mmap 的空间可以独立从系统中分配和释放的系统,对于长时间运行的程序,申请

长生命周期的大内存块就很适合有这种方式。

  1. Mmap 的空间不会被 ptmalloc 锁在缓存的 chunk 中,不会导致 ptmalloc 内存暴增的问题。

坏处有:

  1. 该内存不能被 ptmalloc 回收再利用。
  2. 会导致更多的内存浪费,因为 mmap 需要按页对齐。
  3. 分配效率跟操作系统提供的 mmap()函数的效率密切相关,Linux 系统强制把匿

名 mmap 的内存物理页清 0 是很低效的。

用 mmap 来分配长生命周期的大内存块就是最好的选择,其他情况下都不太高效。

4.M_MMAP_MAX

M_MMAP_MAX 用于设置进程中用 mmap 分配的内存块的最大限制,默认值为 64K,因为有些系统用 mmap 分配的内存块太多会导致系统的性能下降。

如果将 M_MMAP_MAX 设置为 0,ptmalloc 将不会使用 mmap 分配大块内存。

mmap 分配阈值动态调整机制:

当 ptmalloc munmap chunk 时,如果回收的 chunk 空间大小大于 mmap 分配阈值的当前值,并且小于 DEFAULT_MMAP_THRESHOLD_MAX(32 位系统默认为 512KB,64 位系统默认为 32MB),ptmalloc 会把 mmap 分配阈值调整为当前回收的 chunk 的大小,并将 mmap 收缩阈值(mmap trim threshold)设置为 mmap 分配阈值的 2 倍。这就是 ptmalloc 的对 mmap分配阈值的动态调整机制,该机制是默认开启的,当然也可以用 mallopt()关闭该机制

 

为了避免内存暴增,使用应注意几点:

1.后分配的内存先释放。ptmalloc 的内存收缩是从 top chunk 开始,如果与 top chunk 相邻的那个 chunk 在我们 NoSql 的内存池中没有释放,top chunk 以下的空闲内存都无法返回给系统,即使这些空闲内存有几十个 G 也不行。

2.Ptmalloc 不适合用于管理长生命周期的内存,这将导致 ptmalloc 内存暴增。如果要用 ptmalloc 分配长周期内存,在 32 位系统上,分配的内存块最好大于 1MB。这是由于 ptmalloc 默认开启 mmap 分配阈值动态调整功能,1MB 是 32 位系统 mmap 分配阈值的最大值,这样可以保证 ptmalloc 分配的内存一定是从 mmap 映射区域分配的,当 free 时,ptmalloc 会直接把该内存返回给操作系统,避免了被 ptmalloc 缓存。

3.不要关闭 ptmalloc 的 mmap 分配阈值动态调整机制。因为这种机制保证了短生命周期的内存分配尽量从 ptmalloc 缓存的内存 chunk 中分配,更高效,浪费更少的内存。如果关闭了该机制,对大于 128KB 的内存分配就会使用系统调用 mmap 向操作系统分配内存,使用系统调用分配内存一般会比从 ptmalloc 缓存的 chunk 中分配内存慢,特别是在多线程同时分配大内存块时,操作系统会串行调用 mmap(),并为发生缺页异常的页加载新物理页时,默认强制清 0。频繁使用 mmap 向操作系统分配内存是相当低效的。使用mmap 分配的内存只适合长生命周期的大内存块

4,尽量减少程序的线程数量和避免频繁分配/释放内存。Ptmalloc 在多线程竞争激烈的情况下,首先查看线程私有变量是否存在分配区,如果存在则尝试加锁,如果加锁不成功会尝试其它分配区,如果所有的分配区的锁都被占用着,就会增加一个非主分配区供当前线程使用。由于在多个线程的私有变量中可能会保存同一个分配区,所以当线程较多时,加锁的代价就会上升,ptmalloc 分配和回收内存都要对分配区加锁,从而导致了多线程竞争环境下 ptmalloc 的效率降低。

5.防止内存泄露,ptmalloc 对内存泄露是相当敏感的,根据它的内存收缩机制,如果与 top chunk 相邻的那个 chunk 没有回收,将导致 top chunk 一下很多的空闲内存都无法返回给操作系统

6.防止程序分配过多内存,或是由于 Glibc 内存暴增,导致系统内存耗尽,程序因 OOM 被系 统 杀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章