基础概念
为了方便自主管理内存,做法是先向系统申请一块内存,然后将内存切割成小块,通过一定的内存分配算法管理内存。
在64位系统上,Golang程序在启动的时候会向系统申请的内存如下图所示:
预申请的内存划分为spans、bitmap、arena三部分。其中arena即为所谓的堆区,应用中需要的内存从这里分配。spans和bitmap是为了管理arena区而存在的。
- arena的大小为512G,为了方便管理,arena的区域划分成一个个的page,每个page为8kb,一共有512GB/8KB个页。
- spans区域存放span的指针,每个指针对应一个page,所以spans区域的大小为(512GB/8KB)x指针大小8byte = 512MB
- bitmap区域的大小也是通过arena计算出来的,不过主要用于GC。
span
span是用于管理arena页的关键数据结构,每个span中包含一个或者多个连续页,为了满足小对象分配,span中的一页会划分更小的粒度,而对于大对象比如超过页大小,则通过多页实现。
class
根据对象的大小,划分了一系列class,每个class都代表一个固定大小的对象,以及每个span的大小:

- class: class ID,每个span结构中都有一个class ID,表示该span可处理的对象类型。
- bytes/obj:该class代表对象的字节数
- bytes/span:每个span占用堆的字节数,也即页数乘以页大小。
- objects:每个span可分配的对象个数,也即(bytes/spans) / (bytes/obj)。
- waste bytes:每个span产生的内存碎片,也即(bytes/spans) % (bytes/obj)。
上表可见最大的对象是32K大小,超过32K大小的由特殊的class表示,该class ID为0,每个class只包含一个对象。
span的数据结构
span是内存管理的基本单位,每个span用于管理特定的class对象,根据对象大小,span将一个或者多个页拆分成多个块进行管理。
src/runtime/mheap.go:mspan定义了数据结构:
type mspan struct {
next *mspan // 链表向前指针,用于将span链接起来
prev *mspan // 前驱,用于连接span
startAddr uintptr //起始地址,也即所管理页的地址
npages uintptr // 管理的页数
nelems uintptr // 块个数,也即有多少个块可供分配
allocBits *gcBits // 分配位图,每一位代表一个块是否已分配
allocCount uint16 //已分配的个数
spanclass spanClass //class表中的class ID
elemsize uintptr // class表中的对象大小,也即块大小
}以class 10为例,span和管理的内存如下图所示:
spans区域的每个指针对应一个mspan对象,然后mspan对象中存放着有关页的详细数据。spanClass=10,参照上面的表可以得出,npages=1,nelems=56,elemsize=144。其中startAddr是在span初始化时就指定了某个页的地址。allocBits指向一个位图,每位代表一个块是否被分配。本例中已有两个块被分配。
next和prev用于将多个span链接起来,这有利于管理多个span,接下来会进行说明。
cache
有了管理内存的基本单位span,还要有一个数据结构来管理span,这个数据结构叫做mcentral,各线程需要内存时从mcentral管理的span中申请内存,为了避免多线程申请内存时不断的加锁,Golang为每个线程分配了span的缓存,这个缓存就是cache。
数据结构在src/runtime/mcahce.go:mcahce:
type mcache struct {
alloc [67*2]*mspan //按照class分组的mspan列表
}alloc为mspan的指针数组,数组大小为class总数的2倍。数组中每个元素代表了一种类型的span列表,每种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指针,这样可以提高GC扫描性能,对于不包含指针的span列表,没有必要取扫描。
mcache和span的对应关系如下:

mcache在初始化的时候是没有任何span的,在使用过程中会动态的从central中获取并缓存下来,根据使用情况,每种class的span个数也不相同。上图所示,class 0的span数比class1的要多,说明本线程中分配的小对象要多一些。
central
cache作为线程的私有资源为单个线程服务,而central则是全局资源,为多个线程服务,当某个线程内存不足时会向central申请,当某个线程释放内存时又回收进central。
src/runtime/mcentral.go:mcentral定义了central的数据结构:
type mcentral struct {
lock mutex //互斥锁
spanclass spanClass // span class ID
nonempty mSpanList // non-empty指还有空闲块的span列表
empty mSpanList // 指没有空闲块的span列表
nmalloc uint64 //已累计分配的对象个数
}- lock:互斥锁,防止多线程读写冲突
- spanclass:每个mcentral管理着一组由相同class的span列表
- nonempty:指还有内存可用的span列表
- empty:指没有内存可用的span列表
- nmalloc:指累计分配的对象个数
某个线程从central中获取span步骤如下:
- 加锁
- 从nonempty列表获取一个可用span,并将其从链表中删除
- 将取出span放入empty链表
- 解锁
- 线程将该span缓存进cache 而线程归还内存的步骤如下:
- 加锁
- 将span从empty中删除
- 将span加入nonempty列表
- 解锁
heap
从mcentral中可以看到,每个mcentral对象之管理特定的class规格的span。实际上每种class都会对应一个mcentral,这个mcentral的集合存放于mheap中。
src/runtime/mheap.go:mheap定义如下:
type mheap struct {
lock mutex
spans []*mspan
bitmap uintptr // 指向bitmap首地址,bitmap是从高地址向低地址增长的
arena_start uintptr //指示arena区首地址
arena_used uintptr //指示arena区已使用地址位置
central [67*2]struct {
mcentral mcentral
pad [sys.CacheLineSize - unsafe.Szeof(mcentral{})%sys.CacheLineSize]byte
}
}- lock:互斥锁
- spans:指向spans区域,用于映射span和page的关系
- bitmap:bitmap的起始地址。
- arena_start:arena区域首地址
- arena_used:当前arena已使用区域的最大地址
- central:每种class对应的两个mcentrla。
所以从数据结构来看,mheap管理着全部的内存,事实上Golang就是通过一个mheap类型的全局变量进行内存管理的。

内存分配过程
针对待分配对象的大小不同有不同的分配逻辑:
- (0, 16B)且不包含指针的对象:Tiny分配
- (0, 16B)且包含指针的对象:正常分配
[16B, 32KB]:正常分配- (32KB, -):大对象分配 其中Tiny和大对象分配都属于内存管理的优化范畴。以申请size为n的内存为例:
- 获取当前线程私有缓存mcache
- 根据size计算出适合的class ID。
- 从mcache的alloc[class]链表中查询可用的span
- 如果mcache中没有可用的span,则从mcentral申请一个新的span加入mcahce中
- 如果mcentral中也没有可用的span,则从mheap中申请一个新的span加入mcentrla
- 从该span中获取到空闲对象地址并返回。
总结
- Golang程序启动的时候申请一大块内存,并划分为spans、bitmap、arena区域。并使用mheap对整体进行管理。
- arena区域按页划分成一个个小块。
- span管理一个或多个页。
- mcentral管理多个span供线程申请使用
- mcache作为线程私有资源,资源来源于mcentral。