一、概述
(一)基本策略
- 1、程序每次先从系统申请一大块内存(比如1MB),减少向系统申请内存频率,也就是说,先给我整块大的,以后少找你,不够了,再找你要一块大的;
- 2、然后程序将大块内存,按照特定大小(后文将提到的sizeClass,单位可以理解为8字节)切分为小块(object),小块构成链表(span);
- 3、为对象分配内存时,只需要按照对象的大小(sizeClass),从满足该大小的链表中,获取一小块即可;
- 4、回收对象内存时,将该小块内存重新归还到原链表,一遍复用;
- 5、如果闲置的很多,则会尝试归还部分内存给系统,降低程序整体开销
注意上述回收,是指内存分配器的回收,内存分配器只管理内存块,并不关心对象状态,且不会主动回收内存。只有在垃圾回收器完成回收操作后,触发内存分配器回收内存
(二)内存分配器,将内存块分为两种。
- span: 由地址连续的页(page)组成
- object:将span按照特定大小的块切分成多个小块(object),每个小块用于对象存储。按8字节倍数分为N种
是不是有点懵,其实可以这么理解,就像上学时写作业
- 程序向系统申请的一大块内存(比如1MB),这一大块内存,可以看成我们新买了的作业本;
- 接着,制定一些书签,书签上需要标明sizeClass,当后续给不同大小的对象分配内存时,能够快速定位;
- 作业本一些连续的页(page)就组成了span,然后对每页上的一行一行进行分块,可以理解为分object,但是需要根据书签(sizeClass)大小分块,比如书签为1的,我们就像书签下的这些页上的每一行分成一块object
(三)管理组件
Go起点高,直接采用tcMalloc(线程缓存内存)成熟架构
分配器由三种组件组成(组件去管理span,获取和释放object块):
- cache: 每个运行工作线程都会绑定一个cache,用于无锁object分配
- central:为所有cache提供切分好的后备span资源
- heap:管理闲置span,需要时向系统申请或释放内存
(四)几个重要的结构体对象
_NUmsizeClasses = 67
type mspan struct {
next *mspan //双向链表,指向下一个span
prev *mspan
start pageID //起始序号
npages uintptr //页数
freelist gclinkptr //待分配的object 链表
}
type mcache struct {
alloc [_NUmsizeClasses]*mspan //以sizeclass为索引管理多个用于分配的span
}
type mcentral struct {
szieclass int32 //规格大小
noempty mspan //链表,span中还有可用的空闲object
empty mspan //链表,span中没有可用的空闲object
}
type mheap struct {
free [_MaxMHeapList]mspan //页数在127以内的闲置 span 链表数组。因为每页大小是固定的,以page为索引,管理对应的span
freelarge msapn //页数大于127 (大于1MB)span 大链表数组,大对象直接从heap分配回收
central [_NUmsizeClasses]struct {
mcentral mcentral
}
}
分配器按照页数区分不同大小的span,比如 mheap ,以页数为单位将span 存放在管理数组中,需要时就以页数为索引进行查找。当然span大小并非
固定不变,在获取闲置的span时,如果没有找到合适大小的span,那就返回页数更多的span,此时引发裁剪操作,多余部分将构成新的span 被放回管理数组。分配器
还会尝试将相邻的空闲 span 合并,以构成更大的内存块,减少碎片,实现更灵活的分配策略。
用于存储的object,按照8字节倍数分为N种。比如说,大小为24字节的object可以用来存储范围为17——24字节的对象。虽然会有一些浪费,但是分配器只需要面对有限几种规格(sizeclass)的小块内存,优化了分配和复用策略。
同时,分配器会尝试将多个微小的对象组合到一个object内存块,以节约内存
分配器初始化时,会构建对照表,存储大小和规格的对应关系,包括用来切分的span页数(一页8KB)。
若对象大小超出特定的阈值(32KB),会被当做大对象特殊处理。
(五)分配流程
- 1、计算待分配对象对应规格(sizeClass),也就是要几个8字节;
- 2、从cache.alloc 数组中找到对应规格的span;
- 3、从span.freelsit 链表中获取可用的object;
- 4、若没有可用的,即span.freelsit为空,从central获取span
- 5、如central.noempty为空,从heap.free 或者 heap.freelarge 中获取span,并切分object 链表;
- 6、若还是没有找到大小合适且空闲的span,则向操作系统申请新内存块
(六)释放流程
- 1、将标记为可回收的object交给所属span.freelist
- 2、该span被放回central,也就是拼接至mcentral.nonempty链表后,但是不要以为mcache.alloc 数组中就没有该span,
该span还在,任然保持对span的指针引用; - 3、如果span收回了所有的object,则将其还给heap,即mheap.freelist,以便重新分割复用;
- 4、定期扫描heap长时间闲置的span,释放其占用的内存,也就是还给系统
注意,以上不包含大对象,他直接从heap分配和回收
作为工作线程私有且不被共享的cache是实现高性能无锁分配的关键,而central的作用是在多个cache间提高object利用率,避免内存浪费,将span归还heap,是为了在不同规格object需求间平衡。
在计算机科学里,没有什么问题是不能通过中间过程解决的,所以很多架构,都会有中间件这个存在。
假如cache获取span的一部分object后,那么该span中还有许多剩余的object,但是回收操作将该span交还给central,该span还可以给其他线程cache1,cache2...使用,cache并没有持有span,只是用span中object。
而归还到heap的过程,则可以这部分内存,被其他不同规格大小需求使用,重新切分。