堆内存管理介绍
简单的内存管理思路
堆内存管理主要就是做三件事:分配内存块、组织内存块、回收内存块。
分配
每次申请堆内存都从未分配内存中分割出一个小内存块,然后将所有内存块使用链表组织起来。同时需要一些信息来描述每个内存块基本信息,如:大小、是否被使用、下一个内存块地址等...

一个内存块包含3类信息:元数据、用户数据、对齐字段。

释放
把需要释放的内存块从链表中取出来,标记为未使用。当下次需要分配内存块时,可以从未使用的内存块中先查找大小相近的内存块,若找不到再从未分配内存中分配内存。
但是这样的简单的设计随着内存不断的申请和释放,内存会存在大量的内存碎片,降低内存的使用率。
TCMalloc
Linux中有不少的内存管理库,如glibc的ptmalloc、FreeBSD的jemalloc、Google的tcmalloc等。其本质都是为了在再多线程编程下,达到更高的内存管理效率,更快地分配内存。
同一进程的所有线程共享相同的地址空间,它们申请内存时需要加锁,如果不加锁就会出现同一块内存被2个线程同时访问的问题。
TCMalloc的做法是为每一个线程预先分配一块缓存,线程申请小内存时,可以从缓存中分配内存。优点:
- 线程预分配缓存只需要一次系统调用,而线程从预分配的缓存中申请小内存时,都是用户态执行,不需要系统调用。这样缩短了内存总体的分配和释放时间。
- 多线程同时申请小内存,从各自的缓存分配,访问的是不同的地址空间,不需要加锁。降低了内存并发访问的时间。
基本原理

<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">Page</font>:TCMalloc管理内存的基本单位,x64下一个Page默认8KB<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">Span</font>:一个或多个连续的Page组成一个Span。TCMalloc以Span为单位向系统申请内存。

<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">TreadCache</font>:每个线程各自的缓存。一个<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">TreadCache</font>中包含了多个不同级别的空闲内存块链表,同一链表的内存块大小相同,不同链表的内存块大小不同。这样可以在申请内存时,根据申请的内存大小,快速从合适的链表选择空闲内存块。

<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">CentralCache</font>:所有线程共享的缓存,故它的访问需要加锁。其中结构与ThreadCache一致,当ThreadCache的内存块不足时,可以从<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">CentralCache</font>取;ThreadCache内存块过多时,也可以放回<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">CentralCache</font>。<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">PageHeap</font>:堆内存的抽象,把从OS申请出的内存页组织成Span,并保存起来。PageHeap存有多个大小的Span链表。当CentralCache没有内存时,会从PageHeap取,再把1个Span拆分成多个内存块,添加到对应大小的链表中;当CentralCache内存过多时,会放回PageHeap。

不同大小对象分配流程
- 小对象:0-256KB。
ThreadCache -> CentralCache -> HeapPage,大部分时候,ThreadCache缓存都是足够的,不需要去访问CentralCache和HeapPage,无锁分配加无系统调用,分配效率是非常高的。
- 中对象:257KB-1MB
直接从PageHeap选择合适的大小的Span即可。
- 大对象:>1MB
从large span set中选择合适数量的页面组成Span。
Go内存管理

Go的内存管理借鉴了TCMalloc,Go内存管理的许多概念在TCMalloc中已经有了,含义是相同的,只是名字有一些变化。上图基于Go1.11,不同版本Go的内存管理有差异。
Page
与TCMalloc的Page一致,默认8KB
mspan
功能与TCMalloc中的Span类似,由N个连续的页组成,大小为Page大小*N,是Go内存管理的基本单位。
一个mspan会被分为一个个更小粒度的object,至于mspan的object有多大,由该mspan的sizeclass决定。

一个mspan有几个重要的数值概念:
<font style="color:rgb(33, 37, 41);background-color:rgba(0, 0, 0, 0.06);">object size</font>:该mspan中一个object的大小<font style="color:rgb(33, 37, 41);background-color:rgba(0, 0, 0, 0.06);">size class</font>:根据mspan的大小、mspan中一个object的大小而分出的不同类别。mspan有68种size class:0表示>32KB的mspan;1-67表示8B-32KB大小的mspan。
go
字段解释:
class: sizeclass值
bytes/obj: 该`mspan`拆分object大小
bytes/span: 该`mspan`的大小
objects: 该`mspan`共计包含的object数量
tail waste: 该`mspan`拆分为object之后,mspan剩余末尾浪费的内存
// class bytes/obj bytes/span objects tail waste max waste
//
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 24 8192 341 8 29.24%
// 4 32 8192 256 0 21.88%
// 5 48 8192 170 32 31.52%
// 6 64 8192 128 0 23.44%
// 略...
// 62 20480 40960 2 0 6.87%
// 63 21760 65536 3 256 6.25%
// 64 24576 24576 1 0 11.45%
// 65 27264 81920 3 128 10.00%
// 66 28672 57344 2 0 4.91%
// 67 32768 32768 1 0 12.50%
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">span class</font>
因为不包含指针对象的mspan无需GC扫描,根据mspan是否存放包含指针的对象,又可分为2类。
故,一共有68*2=136种不同的类别的mspan。
mspan的类别由其属性中的spanclass表示:
go
// src/runtime/mheap.go:mspan
type mspan struct{
...
spanclass spanClass//spanClass实际是uint8,一共8位
...
}
spanclass的前7位存放size class的值(0-67):。最后一位表示是否需要GC扫描。

mcache
go
// src/runtime/mcache.go:mcache
type mcache struct{
...
tiny uintptr //大小16B,用于分配小于16B的noscan类型
alloc [numSpanClasses]*mspan //136种mspan,每种一个列表
...
}
功能与TCMalloc的ThreadCache类似。
不同点在于:
TCMalloc中每1个线程1个ThreadCache,Go中是每1个P拥有1个mcache。因为最多有GOMAXPROCS个线程在用户态运行,线程的运行又是与P绑定的。最多只需要GOMAXPROCS个mcache就可以保证各个线程对mcache的无锁访问。

central
central是一个68*2=136长的mcentral列表,包含在mheap中:
go
// src/runtime/mheap.go:mheap
type mheap struct{
...
central [numSpanClasses]struct {
mcentral mcentral
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
...
}
其中mcentral:
go
// src/runtime/mcentral.go:mcentral
type mcentral struct {
...
spanclass spanClass //存放的mspan类别
partial [2]spanSet // list of spans with a free object,还没用尽的mspan组成的集合
full [2]spanSet // list of spans with no free objects,已经用尽的mspan组成的集合
...
}

central功能与TCMalloc中的CentralCache类似,不同点在于:
CentralCache是每个类别的Span有1个链表,而mcentral是每个类别的mspan有2个集合。
mheap
mheap与TCMalloc中的PageHeap类似,是整个堆内存的抽象。
Go将堆地址空间划分成了一个个<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0.06);">arena</font>,64位机器上每个arena大小64MB,即8192个page大小。
一个arena对应一个heaparena结构,heaparena中包含了arena的各种元数据。

Go内存分配
对象类型

小对象都在mcache中分配,大对象直接从mheap中分配。
小对象内存分配
为对象寻找mspan
寻找步骤:
- 计算对象的内存大小size
- 按照size->size class的映射,得到sizeclass
- 再根据对象是否含有指针,得到span class
- 从mcache的alloc[span class]链表中查询可用的mspan
从mspan分配对象空间
mspan中的对象内存块,有的被占用,有的未占用。

分配内存时,要找到第一个可用的对象内存块,然后根据该mspan的起始地址计算出该对象内存块的内存地址。
若mcache中该类的mspan都已经满了,mcache就像向central申请一个mspan,拿到mspan后继续分配对象。
mcentral向mcache提供mspan
mcentral会从partial set中分配一个mspan给mcache;mcache中已经填满的mspan也会放入到mcentral的full set中。
若mcentral中也没有mspan了,mcentral会提供需要的page数和span class向mheap申请mspan;mheap没有足够内存则会向OS申请内存页,把申请到的内存页保存到mspan中。
大对象分配
大对象分配则和mcentral向mheap申请内存基本相同。