1.内存分配
1.1 基础概念
编写过C语言的读者一定指导malloc()函数用于动态申请内存,其中内存分配器使用glic提供的ptmalloc2。
内存分配器
- c语言的ptmalloc2
- google的tcmalloc
- facebook的jemalloc
- 后两者在避免内存碎片和性能上均比glibc有较大优势,在多线程下更为明显。
Go语言也实现了内存分配器,原理与tcmalloc类似,简单地来说就是维护一块大的全局内存,每个线程(Go中为处理器P)维护一块小的私有内存,私有内存不足时再从全局申请。
为了方便管理,一般上的做法是向系统申请一块内存,然后将内存切割成小块,通过一定高度内存分配算法管理内存。以64位操作系统为例,Go程序启动时向系统申请的内容如下。
预申请的内存划分为spans、bitmap、arena三部分。
- arena即所谓的堆区
- spans和bitmap是为了管理arena区而存在的
- arena区域划分为一个个的page§,每个页的大小为8KB,一共有64M(512GB/8KB)个页
- spans区域存放span的指针,每个指针对应一个或多个page。所以spans的大小为64M*8byte = 512MB
- bitmap区域的大小也是通过arena计算出来的,不过主要用于GC
1.1.1 span
span是用于管理arena页的关键数据,每个span中包含1个或多个连续页。
- 为了满足小对象分配,则会将span中的一页划分为更小的粒度
- 每一页中只存相同类型的对象
- 对于大对象比如超过页大小,则通过多页实现
span的数据结构
type mspan struct{
next *mspan //链表后向指针
prev *mspan //链表前向指针
startAddr uintptr //起始地址,即所管理页的地址
npages uintptr //管理的页数
nelems uintptr //块个数,即有多少个块可供分配
allocBits *gcBits //分配位图,每一位代表一个块是否已分配
allocCount uint16 //已分配块的个数
spanclass spanClass //class表中的class ID
elemsize uintptr //class表中的对象大小,即块大小
}
1.1.2 cache
有了管理内存的基本单位span,还需要有个结构来管理span。这个数据结构就叫做mcentral,各个线程需要内存时从mcentral管理的span中申请内存,为了避免多线程申请内存时不断地加锁,Go为每个线程分配了span的缓存,这个缓存即cache。
type mcache struct {
alloc [67*2] *mspan // 按class分组的mspan列表
}
alloc为mspan的指针数组,数组大小为class总数的2倍。
- 数组中的每个元素代表一种class类型的span列表
- 每一种class类型都有两组span列表
- 第一组列表中所表示的对象包含了指针
- 第二组列表中所表示的对象不包含指针
- 目的:对于不包含指针的span列表,不需要去做GC扫描
- cache在初始化时是没有任何span的,在使用过程中会动态地从central中获取并缓存下来。
1.1.3 central
cache作为线程的私有资源为单个线程服务,而central则是全局资源,为多个线程服务,当某个线程的内存不足时会向central申请,当某个线程释放内存时又会回收进central。
type mcentral struct{
lock mutex // 互斥锁
spanclass spanClass //span class ID
nonempty mSpanList //指还有空闲块的span列表
empty mSpanList //指没有空闲的span列表
nmalloc uint64 //已累计分配的对象个数
}
- lock:线程间的互斥锁,防止多线程读写冲突
- spanclass:每个mcentral管理一组有相同class的span列表
- nonempty:指还有内存可用的span列表
- empty:指没有内存可用的span列表
- nmalloc:指累计分配的对象个数
线程从central中获取span的步骤如下:
- 加锁
- 从nonempty列表获取一个可用span,并将其从链表中删除
- 将取出的span放入empty链表
- 将span返回给线程
- 解锁
- 线程将该span缓存进cache
线程将span归还的步骤如下:
- 加锁
- 将span从empty列表中删除
- 将span加入nonempty列表
- 解锁
1.1.4 heap
由central的数据结构可见,每个mcentral对象只管理特定的class规格的span。事实上每种class都会对应一个mcentral,这个mcentral的集合存放于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.Sizeof(mcentral{})%sys.CacheLineSize] byte
}
}
- lock:互斥锁
- spans:指向spans区域,用于映射span和page的关系
- bitmap:bitmap的起始地址
- arena_start:arena区域的首地址
- arena_used:当前arena已使用区域的最大地址
- central:每种class对应的两个mcentral
由数据结构可见,mheap管理着全部的内存,事实上Go就上通过一个mheap类型的全局变量进行内存管理的。
2.内存分配的流程
针对待分配对象大小的不同有不同的分配逻辑
- (0,16B)且不包含指针的对象,Tiny分配
- (0,16B)且不包含指针的对象,正常分配
- [16B,32KB]:正常分配
- (32KB,----):大对象分配
Tiny分配和大对象分配都属于内存管理的优化范畴,这里暂时仅关注一般的分配方法。
以申请size为n的内存为例:
- 获取当前线程的私有缓存mcache
- 根据size计算出适合的class的ID
- 从mcache的alloc[class]链表中查询可用的span
- 如果mcache中没有可用的span,从mcentral申请一个新的span加入mcache
- 如果mcahce中也没有可用的span,从mheap中申请一个新的span加入mcentral
- 从该span中获取空闲对象地址并返回
3.小结
Go的内存分配是一个相当复杂的过程,其中还掺杂了GC的处理。
- Go程序启动时申请了一大块内存,划分为spans、bitmap、arena区域
- arena区域按页划分成一个个小块
- span管理一个或多个页
- mcentral管理多个span供线程申请使用
- mcache作为作为线程的私有资源,资源来源于mcentral