池化技术是将程序中需要经常使用的核心资源
先申请出 来,放到一个池
,由程序员自己管理,这样可以提高资源的使用效率,它可以避免核心资源申请和释放带来的开销,也可以保证本程序占有的资源数量。 经常使用的池化技术包括内存池、线程池和连接池
等。
下面我将简要解释常见的池化技术,包括内存池、线程池和连接池:
内存池(Memory Pool): 内存池是一种用于管理内存分配和释放的技术,通过预先分配一定数量的内存块,并在需要时从这些内存块中分配给应用程序。这有助于减少动态内存分配的开销、降低内存碎片,并提高内存管理的效率。
线程池(Thread Pool): 线程池是一组预先创建的线程,它们等待执行任务。当应用程序有任务需要执行时,可以将任务提交到线程池,线程池中的空闲线程将负责执行这些任务。线程池的使用可以避免频繁创建和销毁线程的开销,提高线程的复用率,以及更好地管理并发任务。
连接池(Connection Pool): 连接池用于管理数据库连接或其他资源密集型连接的池化技术。连接池在应用程序启动时预先创建一定数量的连接,并在需要时将这些连接分配给应用程序使用。这减少了每次请求都需要建立新连接的开销,提高了数据库连接的效率。
这些池化技术都是为了提高资源的使用效率、降低开销、提高系统性能而设计的。它们在不同的应用场景中都有广泛的应用,特别是在需要频繁使用和释放资源的情况下,如网络服务器、数据库访问等。
内存池(Memory Pool)是一种动态内存分配与管理技术。 通常情况下,我们可以直接使用 new、 delete、malloc、free 等 API 申请分配和释放内存。这样导致的后果是:当程序长时间运行时,由于所申请内存块的大小不定,频繁使用时会造成大量的内存碎片从而降低程序和操作系统的性能。
内存池则是在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当程序员申请内存时,从池中取出一块内存,当程序员释放内存时,将释放的内存再放入池内,再次申请池可以再取出来使用,并尽量与周边的空闲内存块合并。若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
以下是一些关键点:
内存池的动态管理: 内存池是一种动态内存分配与管理技术。它在程序运行之初申请一大块内存,称为内存池,在程序的整个生命周期内用于分配和释放小块内存。
内存池的备用: 内存池的主要目的是在真正需要内存之前,先预先分配一部分内存,作为备用。当程序需要分配内存时,可以直接从内存池中取出一块内存,而不是使用标准的动态内存分配函数。
内存池的优势: 内存池的使用有助于减少内存碎片,提高内存利用率。由于内存池是在程序运行时预先分配的,因此可以更高效地管理内存,降低内存分配和释放的开销。
自动扩展: 如果内存池的空间不够,一些实现会支持自动扩展内存池的能力。这意味着当内存池中的内存不足以满足分配请求时,内存池可以动态地向操作系统请求更多的内存。
合并空闲内存块: 内存池还可以尽量合并周围的空闲内存块,以提高内存的整体利用效率。
造成堆利用率很低的一个主要原因就是内存碎片化
。内存碎片是指系统中存在未被使用的小块内存,这些小块内存虽然总和可以满足分配请求的总大小,但却无法满足某个具体请求的大小。
内存碎片可以分为两种主要类型:外部碎片和内部碎片。
外部碎片(External Fragmentation): 外部碎片指的是未被使用的、散落在已分配和已使用内存块之间的小块内存。当有一些小块内存散落在内存中,但无法组合成足够大的连续块以满足大块内存请求时,就会发生外部碎片。
内部碎片(Internal Fragmentation): 内部碎片是已经分配给某个特定程序或任务的内存块中未被使用的部分。它是由于内存块的分配方式而导致的,例如分配的内存块可能稍微大于实际需要的内存大小,导致剩余部分无法被有效利用。(假设以前分配了10
个大小的字节,现在只用了5
个字节,则剩下的5
个字节就会内碎片
)。
内存碎片问题的解决方案包括:
内存池管理: 使用内存池技术可以减少内存碎片。内存池通过预先分配一定数量的内存块,然后在应用程序需要时分配这些块,从而减少外部碎片和提高内存利用率。
紧凑算法: 定期执行内存紧缩操作,将已分配的内存块进行整理,以减少外部碎片。这可能涉及将已分配的内存块移到一起,以便形成更大的连续块。
动态内存分配算法: 使用更智能的内存分配算法,避免过度分配导致的内部碎片。例如,使用按需分配的策略,只分配应用程序实际需要的内存大小。
内存压缩: 一些系统可以采用内存压缩技术,将一些不活跃的内存数据进行压缩,以释放出更多的可用内存空间。
解决内存碎片问题对于系统的性能和稳定性都非常重要,特别是在长时间运行的系统中,内存碎片可能会导致内存不足的问题。
使用内存池的主要目的是提高程序的性能和资源利用效率。以下是一些使用内存池的原因:
减少内存碎片: 内存池可以减少内存碎片,因为它在程序启动时预先分配一大块内存,而不是在需要时每次分配小块内存。这有助于减小外部碎片,提高内存利用率。
降低内存分配和释放的开销: 使用标准的动态内存分配函数(如malloc
、free
)可能会涉及到昂贵的系统调用。内存池通过预先分配内存,可以减少频繁的系统调用,从而降低内存分配和释放的开销。
提高内存分配的效率: 内存池可以通过直接从预分配的内存块中获取内存,而无需调用系统调用,提高内存分配的效率。这对于需要频繁分配小块内存的应用场景尤其重要。
资源复用: 内存池可以更好地管理和复用已分配的内存块。内存块的重复利用可以减少动态内存分配和释放的次数,提高系统的性能。
自动内存池扩展: 一些内存池实现支持自动扩展功能,当内存池中的内存不足时,它可以动态地从操作系统中请求更多的内存,而不会导致程序的崩溃。
合并空闲内存块: 内存池可以尝试合并相邻的空闲内存块,从而提高整体内存的利用效率。
一个链表指向空闲内存,分配就是遍历找到一块大小和它一致或者是比它大一些的,取出一块来,然后在修改链表,将剩余的空间挂回到链表中。释放就是放回到链表里面。注意做好标记和保护,避免二次释放,还可以优化如何查找适合大小的内存快的搜索上,减少内存碎片,但是可以增加内存池的外碎片。这种分配器的设计相对简单,适用于一些嵌入式系统或者对内存分配性能要求不高的场景。
以下是一些关键特点和注意事项:
空闲链表: 使用链表来维护空闲内存块的列表。每个节点表示一个空闲内存块,包含起始地址和大小信息。这样的链表可以被遍历,以找到适当大小的内存块。
分配过程: 在分配过程中,遍历链表找到大小符合需求的空闲内存块,然后从中分配一块出去。如果分配的块大小比找到的块大,可以考虑将剩余的部分作为新的空闲块插入到链表中。
释放过程: 将释放的内存块重新插入到空闲链表中。要注意标记已释放的内存块,以防止二次释放的问题。
内存碎片: 这种简单的分配器容易产生内存碎片,即分散的小块内存。为了减少内存碎片,可以考虑使用合并相邻的空闲内存块,或者在分配时尽量选择大小适当的内存块。
性能和复杂度: 这种内存分配器的性能较为简单,但它可能不够高效,特别是在面对大量的内存分配和释放操作时。对于某些应用场景,性能可能不是主要考虑因素,而简洁性和易于理解可能更为重要。
虽然这种内存分配器的设计简单,但在一些特定的嵌入式系统、小型应用或教学用途中可能仍然是有用的。在实际应用中,可能会根据性能需求选择更复杂的内存分配器,比如伙伴分配器、slab 分配器等。
实现一个 FreeList,这个自由链表用于分配固定大小的内存块,比如用于分配 32 字节对象的固定内存分配器。每个内存分配器里面有两个链表。OpenList 用于存储未分配的空闲对象,CloseList 用于存储已分配的内存对象。所谓的分配就是从 OpenList 中取出一个对象放到 CloseList 里并且返回给用户, 释放又是从 CloseList 移回到 OpenList。 分配时内存如果不够,那么就需要增长 OpenList,向系统申请一个更大一点的内存块,切割成相同大小的对象添加到 OpenList 中。这个固定内存分配器回收的时候,统一把先前向系统申请的内存块全部还给系统。
以下是 FreeList 实现的一些关键点:
OpenList 和 CloseList: FreeList 中的两个链表分别用于存储未分配的空闲对象(OpenList)和已分配的内存对象(CloseList)。
分配操作: 分配操作从 OpenList 中取出一个对象,将其移动到 CloseList 中,并返回给用户。如果 OpenList 中没有足够的空闲对象,则需要增长 OpenList。这个过程可以通过向系统申请更大一点的内存块,并切割成相同大小的对象来实现。
释放操作: 释放操作将内存对象从 CloseList 移回到 OpenList 中。这样,对象就可以被重新分配给其他请求。
内存回收: 当固定内存分配器回收时,它将先前向系统申请的内存块全部还给系统。这确保了在 FreeList 不再需要的时候,占用的内存能够完全释放。
优点: 这种固定大小内存分配器的优点在于它的简单性和高效性。它适用于特定场景下对固定大小内存块的高效管理。
缺点: 由于 FreeList 是用于分配固定大小的内存块,它的功能相对单一。此外,它可能占用一定的内存,并且在某些情况下可能会导致内存浪费。
这种实现在特定场景下是非常有效的,尤其是对于需要 频繁分配和释放相同大小内存块 的应用。然而,在其他情况下,可能需要考虑更复杂的内存分配策略,比如适应性分配器或者使用其他数据结构来优化内存管理。
关于内存池内存不够的情况,应该继续向系统去申请:
在定长分配器的基础上,按照不同对象大小(8,16,32,64,128,256,512,1k…64K),构造十多个固定内存分配器,分配内存时根据要申请内存大小进行对齐然后查H表,决定到底由哪个分配器负责,分配后要在内存头部的 header 处写上 cookie,表示由该块内存哪一个分配器分配的,这样释放时候你才能正确归还。如果大于 64K,则直接用系统的 malloc 作为分配,如此以浪费内存为代价你得到了一个分配时间近似 O(1) 的内存分配器。这种内存池的缺点是假设某个 FreeList 如果高峰期占用了大量内存即使后面不用,也无法支援到其他内存不够的 FreeList,达不到分配均衡的效果。
以下是一些关键点的总结:
多种定长内存分配器: 针对不同的对象大小,构建了多个固定内存分配器,每个分配器负责一定范围大小的内存分配。这可以有效地降低内存碎片,提高内存的利用效率。
哈希映射: 使用哈希表来映射不同大小的内存块到相应的定长内存分配器。当需要分配内存时,通过对齐并查找哈希表,确定由哪个分配器负责。
Cookie 标记: 在分配的内存块的头部添加 Cookie,用于标记由哪个分配器分配的。这样可以确保在释放时能够正确归还到相应的定长内存分配器。
大内存直接使用系统 malloc: 当申请的内存大小超过一定阈值(64K),直接使用系统的 malloc
,避免切分大块内存导致的内存碎片问题。
分配均衡问题: 由于每个分配器负责一定范围的大小,可能存在某个分配器高峰期占用了大量内存,导致其他分配器无法充分利用空闲内存。这是一个分配均衡的问题,有可能会浪费一些内存。
多线程并发: 在多线程并发场景下,需要考虑线程安全性。可以通过加锁等手段解决,但是锁的激烈竞争可能会降低分配释放效率。
在实际应用中,需要根据具体的需求和性能要求权衡各种因素,例如内存利用率、分配速度、线程安全性等。这种内存分配器在解决了一些问题的同时,也引入了一些新的挑战,需要根据实际情况进行调优和权衡。
优点:
缺点:
malloc
,而不是通过内存分配器,这可能会导致无法充分利用内部 FreeList 中的空闲内存。注:这种设计和 STL 库的耳机空间配置器的设计完全一样。
malloc 又称显示动态存储器分配器,动态存储器分配器维护着一个进程的虚拟存储器区域,称为堆。
我们假设堆紧接着未初始化.bss段后开始,并向上生长,对于每个进程,由内核维护着堆顶(brk —- break)
我们在 UNIX 系统下讨论 malloc 如何分配空间
#include <stdlib.h>
void* malloc(size_t size);
正如我们平时所使用一样,malloc 函数返回一个指针,指向大小(至少)为 size 字节的存储器块,这个块可能会包含在这个块内的任何数据对象类型做对齐。
(在 UNIX 系统上,malloc 返回一个 8 字节边界对齐的块)
#include <unistd.h>
void *sbrk(intptr_t incr);
#include <stdlib.h>
void free(void *ptr);
现在我们展示 malloc free 是如何管理一个 C 程序的堆的,每个方框代表一个 4 字节的字。
处理任意请求序列
一个应用可以有任意的分配请求和释放请求序列。
立即响应
分配器必须立即相应分配需求。
只使用堆
分配器使用的任何非标量数据都必须保存在堆里。
对齐
分配器必须对齐块,这是为了使得他们可以保存任何类型的数据对象。
不修改已分配块
分配器只能对空闲块进行操作。
最大化吞吐量
一个分配请求的最糟糕运行时间与空闲块的数量成线性关系,但释放请求的运行时间是个常数。
最大化存储器利用率
由于虚拟存储器的数量是受磁盘上交换空间的数量限制的,所以必须高效的使用。
而分配器则是在这两个要求之间找到一个合适的平衡。
碎片是造成堆利用率很低的一个主要原因。当有未使用的存储器但不能来满足分配请求时,就会发生这种现象。
碎片分为 :内部碎片和外部碎片
在这里我们先思考 一个动态分配器需要做的事情,并且规划出一个蓝图。
由于外部碎片的难以量化和不可预测,所以分配器通常维持少量的大空闲块,而不是维持大量的小空闲块。
在实现时我们需要考虑:
我们用一个数据结构来描述我们的空闲块,包括块的边界,以及区别已分配和空闲块。然后将这个数据结构用链表进行维护。
如果我们要强加一个双字的对齐约束条件,那么块的大小应该是 8 的倍数。
在头部后面就应该是调用 malloc 时请求的有效核载,有效核载后面是一片不使用的填充块(分配器策略或用于满足对其要求)。
这样我们就可以利用上述的头部来将堆组织为一个连续已分配和空闲块的序列,其中彩色块代表已分配。空白代表空闲 。
在这里我们并不需要一个前后指针来指向下一个空闲节点/分配节点,只需要读出头部的块大小并以当前地址为起始+块大小就可以计算出下一个空闲块/分配块的地址。
这样的结构就被称为隐式空闲链表。因为空闲块是通过头部中的大小字段隐含地连接着的 ,从而使得分配器通过遍历堆中的所有块,从而间接的遍历整个空闲块的集合。
但是隐式空闲链表也有一个明显的缺点就是,当我们要分配块时,空闲链表的搜索与堆中已分配块和空闲块的总数呈线性关系。
在隐式空闲链表中,由于块的分配与堆块的总数呈线性关系,所以对于通用分配器来说,隐式空闲链表是不合适的。
如果我们将空闲块组织为某种显示的数据结构,由于程序不需要一个空闲块的主题,所以我们将数据结构的指针存放在空闲块的主体里面,我们将堆组织为一个双向空闲链表,在每个空闲块中都包含一个 pred 前驱和 succ 后继指针。
使用双向链表后,使得首次适配的分配时间从块总数的线性时间减少到空闲块数量的线性时间。不过释放一个块的时间可以是线性的,也可以是常数的。
在显示空闲链表中,一个使用单向空闲块链表的分配器需要与空闲块数量呈线性关系的时间来分配块。
而分离,就是维护多个空闲链,其中每个链表中的块有大致相等的大小,一般是将所有可能的块分成一些等价类。
分配器维护着一个空闲链表数组 ,每个大小类一个空闲链表,按照大小的升序排列。当分配器需要一个大小为 n 的块时,他就搜索相应的空闲链表,如不能找到则搜索下一个链表,以此类推。
每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。
为了分配给一个给定大小的块,我们检查相应的空闲链表,如果链表为空,我们简单地分配其中第一块的全部。此时空闲块是不会分割以满足分配请求的。如果链表为空,分配器就向操作系统申请一个固定大小的额外存储器片,将这个片分成大小相等的块,并将这些块链接起来形成新的空闲链表。释放时,只需要简单的将这个块插入到相应的空闲链表前部。
这样的话,分配和释放都可以在常数时间内完成。由于我们不进行分割,那么也就没有合并,所以我们就不需要一个已分配/空闲标记,已分配块也就不需要头部,因为没有合并,同样也不需要脚部。
缺点 :容易造成外部碎片和内部碎片。
当一个应用请求一个 k 字节的块时,分配器搜索空闲链表,查找一个可以放置所请求块的空闲块。这就和分配器的放置策略相关联了。
一旦分配器找到一个匹配的空闲块,那么此时需要考虑的就是,分配这个空闲块中的多少空间。
如果选择使用整个空闲块,虽然速率较快,但是会造成内部碎片 。(但是如果趋向于产生好的匹配,那么内部碎片可以接受)。
如果匹配的不太好,分配器通常会选择将空闲块一分为二 ,第一部分变成分配块,而剩下的部分变成一个新的空闲块 。
当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻。这些相邻的空闲块可能会造成一种 ‘假碎片’ 现象(有许多可用的空闲块被切割为小的无法使用的空闲块)。
为了解决这一个问题,任何分配器都必须执行合并相邻的空闲块,这个过程就被称为合并。
分配器可以选择立即合并,也可以选择推迟合并。
那么分配器如何实现合并?
以我们前面所设计的数据结构为例,当我们释放一个分配块时,合并下一个空闲块很简单且高效,但是如何合并前面的块就成了一个问题,所以我们需要对前面所设定的数据结构加以改进。
在这里我们添加了一个脚部,那么分配器就可以通过检查它的脚部来判断前一个块的起始位置。
但是这样会造成,我们的每个块都保持一个头部和一个脚部,如果一个应用程序大量的申请小块空间时,会产生显著的存储器开销。
所以我们需要对前面的头部+脚部的形式进行改进。
因为我们只有在合并的时候才会使用到脚部,所以对于已分配的块只需要一个头部而不需要脚部,但是空闲块依然需要脚部。
C 标准库 函数 malloc 在底层使用的是 —– 分离适配
使用这种方法,分配器维护着一个空闲链表数组,每个空闲链表被组织成某种类型的显示/隐式链表。每个链表包含大小不同的块,这些块的大小是大小类的成员。
当要分配一个块时,我们确定了大小类之后,对适当的空闲链表做首次适配,查找一个合适的块,如果找到,那么可选地分割它,并将剩余的部分插入到适当的空闲链表中。如果没找到,那就搜索下一个更大的大小类的空闲链表,重复直到找到一个合适的块。如果空闲链表中没有合适的块,那么就向操作系统请求额外的堆存储器,从这个新的堆存储器中分配一个块,将剩余部分放置在适当的大小类中。
当释放一个块时,我们执行合并,并将结果放在相应的空闲链表中。
优点 :
现在大部分的开发环境都是多核多线程,在申请内存的场景下,必然存在激烈的锁竞争问题。要实现一个高并发的内存池,必须要考虑以下几个问题:
主要由三部分组成:
注:怎么实现每个线程都拥有自己唯一的线程缓存呢?
为了避免加锁带来的效率,在 Thread Cache 中使用 thread local storage 保存每个线程本地的 ThreadCache 的指针,这样Thread Cache 在申请释放内存是不需要锁的。因为每一个线程都拥有了自己唯一的一个全局变量。
class ThreadCache
{
public:
// 分配内存
void* Allocate(size_t size);
// 释放内存
void Deallocate(void* ptr, size_t size);
// 从中心缓存中获取内存对象
void* FetchFromCentralCache(size_t index, size_t size);
// 当自由链表中的对象超过一次分配给threadcache的数量,则开始回收
void ListTooLong(FreeList* freelist, size_t byte);
private:
FreeList _freelist[NLISTS];// 创建了一个自由链表数组
};
关于 FreeList 这个类,我们只要封装一个普通指针和链表的长度即可。
Thread Cache 申请内存:
Thread Cache释放内存:
小于64k
时将内存释放回thread cache,先计算size在自由链表中的位置,然后将对象Push到 FreeList[i] // 控制内碎片浪费不要太大
//[1, 128] 8byte对齐 freelist[0,16)
//[129, 1024] 16byte对齐 freelist[17, 72)
//[1025, 8 * 1024] 64byte对齐 freelist[72, 128)
//[8 * 1024 + 1, 64 * 1024] 512byte对齐 freelist[128, 240)
// 也就是说对于自由链表数组只需要开辟240个空间就可以了
// 大小类
class ClassSize
{
public:
// align 是对齐数
static inline size_t _RoundUp(size_t size, size_t align)
{
// 比如 size 是15 < 128,对齐数 align 是8,那么要进行向上取整,
// ((15 + 7) / 8) * 8就可以了
// 这个式子就是将(align - 1)加上去,这样的话就可以进一个对齐数
// 然后再将加上去的二进制的低三位设置为0,也就是向上取整了
// 15 + 7 = 22 : 10110 (16 + 4 + 2)
// 7 : 111 ~7 : 000
// 22 & ~7 : 10000 (16)就达到了向上取整的效果
return (size + align - 1) & ~(align - 1);
}
// 向上取整
static inline size_t RoundUp(size_t size)
{
assert(size <= MAXBYTES);
if (size <= 128)
{
return _RoundUp(size, 8);
}
if (size <= 8 * 128)
{
return _RoundUp(size, 16);
}
if (size <= 8 * 1024)
{
return _RoundUp(size, 128);
}
if (size <= 64 * 1024)
{
return _RoundUp(size, 512);
}
else
{
return -1;
}
}
//求出在该区间的第几个
static size_t _Index(size_t bytes, size_t align_shift)
{
//对于 (1 << align_sjift) 相当于求出对齐数
//给 bytes 加上对齐数减一也就是,让其可以跨越到下一个自由链表的数组的元素中
return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}
//获取自由链表的下标
static inline size_t Index(size_t bytes)
{
//开辟的字节数,必须小于可以开辟的最大的字节数
assert(bytes < MAXBYTES);
//每个对齐区间中,有着多少条自由链表
static int group_array[4] = { 16, 56, 56, 112 };
if (bytes <= 128)
{
return _Index(bytes, 3);
}
else if (bytes <= 1024) // (8 * 128)
{
return _Index(bytes - 128, 4) + group_array[0];
}
else if (bytes <= 4096) // (8 * 8 * 128)
{
return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
}
else if (bytes <= 8 * 128)
{
return _Index(bytes - 4096, 9) + group_array[2] + group_array[1] + group_array[0];
}
else
{
return -1;
}
}
};
注:什么是 span ? 一个 span 是由多个页组成的一个 span 对象。一页大小是 4k。
// span 结构
// 对于 span 是为了对于 thread cache 还回来的内存进行管理
// 一个 span 中包含了内存块
typedef size_t PageID;
struct Span
{
PageID _pageid = 0; // 起始页号(一个 span 包含多个页)
size_t _npage = 0; // 页的数量
Span* _next = nullptr; // 维护双向span链表
Span* _prev = nullptr;
void* _objlist = nullptr; //对象自由链表
size_t _objsize = 0; //记录该 span 上的内存块的大小
size_t _usecount = 0; //使用计数
};
关于 spanlist,设计为一个双向链表,插入删除效率较高:
class SpanList
{
public:
// 双向循环带头结点链表
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
Span* begin()
{
return _head->_next;
}
Span* end()
{
return _head;
}
bool Empty()
{
return _head == _head->_next;
}
void Insert(Span* cur, Span* newspan)
{
assert(cur);
Span* prev = cur->_prev;
//prev newspan cur
prev->_next = newspan;
newspan->_prev = prev;
newspan->_next = cur;
cur->_prev = newspan;
}
void Erase(Span* cur)
{
assert(cur != nullptr && cur != _head);
Span* prev = cur->_prev;
Span* next = cur->_next;
prev->_next = next;
next->_prev = prev;
}
void PushBack(Span* cur)
{
Insert(end(), cur);
}
void PopBack()
{
Span* span = end();
Erase(span);
}
void PushFront(Span* cur)
{
Insert(begin(), cur);
}
Span* PopFront()
{
Span* span = begin();
Erase(span);
return span;
}
// 给每一个 Spanlist 桶加锁
std::mutex _mtx;
private:
Span * _head = nullptr;
};
Central Cache 申请内存:
Central Cache 释放内存:
注:怎么才能将 Thread Cache 中的内存对象还给他原来的 span 呢?
答:可以在 Page Cache 中维护一个页号到 span 的映射,当 Span Cache 给 Central Cache 分配一个 span 时,将这个映射更新到 map 中去,在 Thread Cache 还给 Central Cache 时,可以查这个 map 找到对应的 span。
页
为单位的 span 自由链表Page cache
,这个类可以被设计成了单例模式// 采用饿汉模式,在 main 函数之前单例对象已经被创建
class PageCache
{
public:
// 获取单例模式
static PageCache* GetInstance()
{
return &_inst;
}
// 在 SpanList 中获取一个 span 对象,如果没有或者申请内存大于128页,则直接去系统申请
Span* NewSpan(size_t npage);
Span* _NewSpan(size_t npage);
// 获取从对象到 span 的映射
Span* MapObjectToSpan(void* obj);
// 从 CentralCache 归还 span 到 Page,然后 PageCache 进行合并
void RelaseToPageCache(Span* span);
private:
// NPAGES 是129,最大页数为128,也就是下标从1开始到128分别为1页到128页
SpanList _pagelist[NPAGES];
private:
PageCache() = default;
PageCache(const PageCache&) = delete;
PageCache& operator=(const PageCache&) = delete;
static PageCache _inst;
// 为了锁住 SpanList,可能会存在多个线程同时来 PageCache 申请 span
std::mutex _mtx;
std::unordered_map<PageID, Span*> _id_span_map;
};
PageCache 申请内存:
PageCache 释放内存:
如果 CentralCache 释放回一个 span,则依次寻找 span 的前后 page id 的 span,看是否可以合并,如果能够合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。但是合并的最大页数超过128页,则不能合并。
如果 ThreadCache 想直接申请大于 64k 的内存,直接去 PageCache 去申请,当在 PageCache 申请时,如果申请的内存大于128页,则直接向系统申请这块内存,如果小于128页,则去 SpanList 去查找。
关于brk参考:https://www.cnblogs.com/vinozly/p/5489138.html
文章参考:
http://t.csdnimg.cn/1YSKk
http://t.csdnimg.cn/nRN4y