简介
golang runtime的另外一大主题就是内存分配器,内存分配策略与协程栈、堆、GC等话题息息相关。
- 类似于TC malloc的思想;
- 使用span机制来减少内存碎片,每个span至少为一个页(go中的一个page为8KB),且大小为页的整数倍,每一种span用于一个范围的内存分配需求. 比如16-32byte使用分配32byte的span, 112-128使用分配128byte的span.
- 一共有67个size范围, 8byte-32KB;每个size有两种类型(scan和noscan, 表示分配的对象是否会包含指针,不包含指针的就不用GC scan)
- 多层次Cache来减少分配的冲突。 per-P无锁的mcache,全局67*2个对应不同size的span的后备mcentral, 全局1个的mheap.
- mheap中以 treap 的结构维护空闲连续page. 归还内存到heap时, 连续地址会进行合并.
- stack分配也是多层次和多class的.
- 对象由GC进行回收. sysmon会定时把空余的内存归还给操作系统
golang的内存分配器虽然思想来源于tcmalloc但是实际上与tcmalloc有很大区别,其中很重要一点是Go 语言被设计为没有显式的内存分配与释放,完全依靠编译器与 runtime 的配合来自动处理,因此也就造就为了内存分配器、垃圾回收器两大组件。
在计算机领域性能优化基本离不开空间换时间,时间换空间,统一管理内存会提前分配或一次性释放一大块内存,进而减少与操作系统沟通造成的开销,进而提高程序的运行性能。支持内存管理另一个优势就是能够更好的支持垃圾回收,这一点我们留到垃圾回收器一节中进行讨论。
内存分配器主要结构
核心的结构就是:
- heapArena: 保留整个虚拟地址空间
- mspan:是 mheap 上管理的一连串的页
- mheap:分配的堆,在页大小为 8KB 的粒度上进行管理
- mcentral:搜集了给定大小等级的所有 span
- mcache:为 per-P 的缓存。
页是向操作系统申请内存的最小单位,目前设计为 8kb。
这些结构之间的关系比较复杂,后面我们将一点点梳理他们之间的关系。
在golang里面内存分为部分,传统意义上的栈由 runtime 统一管理,用户态不感知。而传统意义上的堆内存,又被 Go 运行时划分为了两个部分,
- 一个是 Go 运行时自身所需的堆内存,即堆外内存;
- 另一部分则用于 Go 用户态代码所使用的堆内存,也叫做 Go 堆。
Go 堆负责了用户态对象的存放以及 goroutine 的执行栈。
heapArena
Golang 的堆由很多个 arena 组成,每个 arena 在 64 位机器上是 64MB,且起始地址与 arena 的大小对齐,
所有的 arena 覆盖了整个 Golang 堆的地址空间。
heapArena 对象存储了一个 heap arena的元数据,heapArena对象自身存储在Go heap之外,并且通过mheap\_.arenas index 来访问。heapArena对象直接从操作系统分配的,所以理想情况下应该是系统页面大小的倍数。
const(
pageSize = 8192//8KB
heapArenaBytes = 67108864 //一个heapArena是64MB
heapArenaBitmapBytes = heapArenaBytes / 32 // 一个heapArena的bitmap占用2MB
pagesPerArena = heapArenaBytes / pageSize // 一个heapArena包含8192个页
)
//go:notinheap
type heapArena struct {
bitmap [heapArenaBitmapBytes]byte //2,097,152
spans [pagesPerArena]*mspan //
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
}
- bitmap:是一个2MB个byte数组来标记这个heap area 64M 内存的使用情况,bitmap位图主要为GC标记数组,用2bits标记8(PtrSize) 个byte的使用情况。之所以用2个bits,一是标记对应地址中是否存在对象,另外是标记此对象是否被gc标记过。一个功能一个bit位,所以, heap bitmaps用两个bit位;
- spans:是一个8192(pagesPerArena)大小的指针数组,每个mspan是8KB;
- pageInUse:是一个位图,使用1024 * 8 bit来标记 8192个页(8192*8KB = 64MB)中哪些页正在使用中;
- pageMarks:标记页,与GC相关;
简而言之,heapArena 描述了一个 heap arena 的元信息。
arenaHint
arenaHint结构比较简单,是 arenaHint 链表的节点结构,保存了arena 的起始地址、是否为最后一个 arena,以及下一个 arenaHint 指针。
//go:notinheap
type arenaHint struct {
addr uintptr
down bool
next *arenaHint
}
mspan
前面说了,heapArena 的内存大小是64M,直接管理这么粗粒度的内存明显不符合实践。golang使用span机制来减少碎片. 每个span至少分配1个page(8KB), 划分成固定大小的slot, 用于分配一定大小范围的内存需求,小于 32kb 的小对象则分配在固定大小等级的 span 上,否则直接从 mheap 上进行分配。
mspan
是相同大小等级的 span 的双向链表的一个节点,每个节点还记录了自己的起始地址、指向的 span 中页的数量。
//go:notinheap
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
startAddr uintptr // address of first byte of span aka s.base()
npages uintptr // number of pages in span
//......
freeindex uintptr
//......
allocCount uint16 // number of allocated objects
spanclass spanClass // size class and noscan (uint8)
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
elemsize uintptr // computed from sizeclass or from npages
}
- npages:表示当前span包含多少个页,npages是根据spanclass来确定的。前面说过了,一个页是8k,也就是这个span存储的是 npages*8k 大小内存。
- spanclass:spanClass是一个uint8,用于计算当前span分配对象的大小。spanClass 的值为0-66,每一个值分别对应一个分配对象的大小以及页数。比如spanclass为1,则span用于分配8个字节的对象,且当前span占用一个页的存储,也就是span是8kb。
- elemsize:表示分配对象的size,根据spanclass和npages都能够算出来。
这里举一个例子:32byte的span,span占用一个页,所以总共有256个slot:
- 这里表示slot大小为32byte的span, 上一次gc之后, 前8个slot使用如上.
- freeindex表示 <该位置的都被分配了, >=该位置的可能被分配, 也可能没有. 配合allocCache来寻找. 每次分配后, freeindex设置为分配的slot+1.
- allocBits表示上一次GC之后哪一些slot被使用了. 0未使用或释放, 1已分配.
- allocCache表示从freeindex开始的64个slot的分配情况, 1为未分配, 0为分配. 使 用ctz(Count Trailing Zeros指令)来找到第一个非0位. 使用完了就从allocBits加载, 取 反.
- 每次gc完之后, sweep阶段, 将allocBits设置为gcmarkBits.
前面一直都说,spanclass可以确定当前span的page数以及分配的对象的大小:
// sizeclasses.go
// 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 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// 11 160 8192 51 32 9.73%
// 12 176 8192 46 96 9.59%
// 13 192 8192 42 128 9.25%
// 14 208 8192 39 80 8.12%
// 15 224 8192 36 128 8.15%
// 16 240 8192 34 32 6.62%
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
// 26 576 8192 14 128 12.33%
// 27 640 8192 12 512 15.48%
// 28 704 8192 11 448 13.93%
// 29 768 8192 10 512 13.94%
// 30 896 8192 9 128 15.52%
// 31 1024 8192 8 0 12.40%
// 32 1152 8192 7 128 12.41%
// 33 1280 8192 6 512 15.55%
// 34 1408 16384 11 896 14.00%
// 35 1536 8192 5 512 14.00%
// 36 1792 16384 9 256 15.57%
// 37 2048 8192 4 0 12.45%
// 38 2304 16384 7 256 12.46%
// 39 2688 8192 3 128 15.59%
// 40 3072 24576 8 0 12.47%
// 41 3200 16384 5 384 6.22%
// 42 3456 24576 7 384 8.83%
// 43 4096 8192 2 0 15.60%
// 44 4864 24576 5 256 16.65%
// 45 5376 16384 3 256 10.92%
// 46 6144 24576 4 0 12.48%
// 47 6528 32768 5 128 6.23%
// 48 6784 40960 6 256 4.36%
// 49 6912 49152 7 768 3.37%
// 50 8192 8192 1 0 15.61%
// 51 9472 57344 6 512 14.28%
// 52 9728 49152 5 512 3.64%
// 53 10240 40960 4 0 4.99%
// 54 10880 32768 3 128 6.24%
// 55 12288 24576 2 0 11.45%
// 56 13568 40960 3 256 9.99%
// 57 14336 57344 4 0 5.35%
// 58 16384 16384 1 0 12.49%
// 59 18432 73728 4 0 11.11%
// 60 19072 57344 3 128 3.57%
// 61 20480 40960 2 0 6.87%
// 62 21760 65536 3 256 6.25%
// 63 24576 24576 1 0 11.45%
// 64 27264 81920 3 128 10.00%
// 65 28672 57344 2 0 4.91%
// 66 32768 32768 1 0 12.50%
const (
_MaxSmallSize = 32768
smallSizeDiv = 8
smallSizeMax = 1024
largeSizeDiv = 128
_NumSizeClasses = 67
_PageShift = 13
)
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
主要就是数组class\_to\_size和数组class\_to\_allocnpages。 数组的size 都是67,也就是0-66。index对应的value分别就是对象的大小以及span占用page数目。
class0表示分配一个>32KB对象的span, 有67个 size, 每个size两种, 用于分配有指针和无指针对象, 所以代码里面总共有67*2=134个class。比如 spanClass 是1,那么对应分配对象就是8bytes,然后一个span占用 一个页内存,也就是8Kb。
mcentral
// Central list of free objects of a given size.
//go:notinheap
type mcentral struct {
lock mutex
spanclass spanClass
nonempty mSpanList // list of spans with a free object, ie a nonempty free list
empty mSpanList // list of spans with no free objects (or cached in an mcache)
// nmalloc is the cumulative count of objects allocated from
// this mcentral, assuming all spans in mcaches are
// fully-allocated. Written atomically, read under STW.
nmalloc uint64
}
当 mcentral 中 nonempty 列表中也没有可分配的 span 时,则会向 mheap 提出请求,从而获得新的 span,并进而交给 mcache。
mcache
mcache是一个 per-P 的缓存,它是一个包含不同大小等级的 span 链表的数组,其中 mcache.alloc 的每一个数组元素都是某一个特定大小的 mspan 的链表头指针。
const numSpanClasses = _NumSizeClasses << 1 // means (67<<1)
// Per-thread (in Go, per-P) cache for small objects.
// No locking needed because it is per-thread (per-P).
//
// mcaches are allocated from non-GC'd memory, so any heap pointers
// must be specially handled.
//
//go:notinheap
type mcache struct {
......
// Allocator cache for tiny objects w/o pointers.
// See "Tiny allocator" comment in malloc.go.
// tiny points to the beginning of the current tiny block, or
// nil if there is no current tiny block.
//
// tiny is a heap pointer. Since mcache is in non-GC'd memory,
// we handle it by clearing it in releaseAll during mark
// termination.
tiny uintptr
tinyoffset uintptr
local_tinyallocs uintptr // number of tiny allocs not counted in other stats
// The rest is not accessed on every malloc.
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
stackcache [_NumStackOrders]stackfreelist
// Local allocator stats, flushed during GC.
local_largefree uintptr // bytes freed for large objects (>maxsmallsize)
local_nlargefree uintptr // number of frees for large objects (>maxsmallsize)
local_nsmallfree [_NumSizeClasses]uintptr // number of frees for small objects (<=maxsmallsize)
......
}
当 mcache 中 span 的数量不够使用时,会向 mcentral 的 nonempty 列表中获得新的 span。
mheap
//go:notinheap
type mheap struct {
lock mutex
free mTreap // free spans
......
allspans []*mspan // all spans out there
......
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
......
arenaHints *arenaHint
arena linearAlloc
......
// central free lists for small size classes.
// the padding makes sure that the mcentrals are
// spaced CacheLinePadSize bytes apart, so that each mcentral.lock
// gets its own cache line.
// central is indexed by spanClass.
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
spanalloc fixalloc // allocator for span*
cachealloc fixalloc // allocator for mcache*
treapalloc fixalloc // allocator for treapNodes*
specialfinalizeralloc fixalloc // allocator for specialfinalizer*
specialprofilealloc fixalloc // allocator for specialprofile*
speciallock mutex // lock for special record allocators.
arenaHintAlloc fixalloc // allocator for arenaHints
......
}
各种结构之间的关系
heap是中间的一行:
- 其中最中间的灰色区域 arena 覆盖了 Go 程序的整个虚拟内存,每个 arena 包括一段 bitmap 和一段指向连续 span 的指针;
- 每个 span 由一串连续的页组成;
- 每个 arena 的起始位置通过 arenaHint 进行记录。
分配的整体顺序是从右向左,代价也越来越大。
- 小对象和微对象优先从白色区域 per-P 的 mcache 分配 span,这个过程不需要加锁(白色);
- 若失败则会从 mheap 持有的 mcentral 加锁获得新的 span,这个过程需要加锁,但只是局部(灰色);
- 若仍失败则会从右侧的 free 或 scav 进行分配,这个过程需要对整个 heap 进行加锁,代价最大(黑色)。
内存分配入口
golang程序的运行是基于 goroutine 的,goroutine 和传统意义上的程序一样,也有栈和堆的概念,在
Go runtime 内部分别对应:goroutine 执行栈以及 Go 堆。goroutine 的执行栈和我们传统意义上的栈一样,当函数返回时,在栈的对象都会被自动回收,从而无需 GC 的标记;而堆则麻烦一些,Go 支持垃圾回收,只要对象生存在堆上,Go 的runtime GC 会在后台自动进行标记、整理以及在垃圾回收时候回收内存,GC的存在会导致额外的开销。
举个简单的程序:
func f1() *int {
y := 2
return &y
}
func main() {
y := f1()
println(y)
}
go build -gcflags '-m -l -N' memory_alloc.go
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。