原文地址
# Golang 内存模型
堆是 Go 运行时中最大的临界共性资源,这意味着每次存取都要加锁,在性能层面是很恐怖的事情。为解决这个问题,Golang 在堆 mheap 之上,一次细化粒度,建立了 mcentral、mcache 的模型
- mheap: 全局内存起源,访问要加全局锁
- mcentral: 每种对象大小规格(全局共花纹为 68 种)对应的缓存,锁的粒度也仅限于同一种规格以内
- mcache: 每个 P(即 GMP 中的 P)持有一份的内存缓存,访问时无锁
# 核心概念
# 内存单元 mspan
mspan是一个双向链表结构。mspan是 golang 中内存管理的最小单元。mspan大小是 page 的整数倍(Go 中 page 大小为 8KB),且内部的页是连续的- 每个
mspan根据空间大小以及面向分配对象的大小,会被划分为不同的等级 - 同等级的 mspan 会从属同一个 mcentral,最终会被组织称链表,因此带有前后指针(prev、next)
- 由于同等级的 mspan 内聚于同一个 mcentral,所以会基于同一把互斥锁管理
- mspan 会基于 bitMap 辅助快速找到空闲内存块(块大小为对应等级下的 object 大小),此时需要使用到 Ctz64 算法
type mspan struct { | |
next *mspan // 指向下一个 mspan | |
prev *mspan // 指向上一个 mspan | |
list *mSpanList | |
startAddr uintptr // 起始地址 | |
npages uintptr // 页数 | |
manualFreeList gclinkptr // 空闲对象列表 | |
freeindex uint16 | |
nelems uint16 //span 链表中元素个数 | |
// 标识此前的位置都已被占用 | |
freeIndexForScan uint16 // | |
allocBits *gcBits // 表示该 span 种所有元素的使用分配情况,位值置为 1 则标识 span 链表中对应位置的 span 已被分配 | |
gcmarkBits *gcBits // | |
pinnerBits *gcBits | |
sweepgen uint32 | |
divMul uint32 // for divide by elemsize | |
allocCount uint16 // 已分配的对象个数 | |
spanclass spanClass //span 类别 | |
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods) | |
needzero uint8 // needs to be zeroed before allocation | |
isUserArenaChunk bool // whether or not this span represents a user arena | |
allocCountBeforeCache uint16 // a copy of allocCount that is stored just before this span is cached | |
elemsize uintptr // 能存储的对象大小 | |
limit uintptr // end of data in span | |
speciallock mutex // guards specials list and changes to pinnerBits | |
specials *special // linked list of special records sorted by offset. | |
userArenaChunkFree addrRange // interval for managing chunk allocation | |
largeType *_type // malloc header for large objects. | |
} |
# 内存单元等级 spanClass
| class | bytes/obj | bytes/span | objects | tail waste | max waste | min align |
|---|---|---|---|---|---|---|
| 1 | 8 | 8192 | 1024 | 0 | 87.50% | 8 |
| 2 | 16 | 8192 | 512 | 0 | 43.75% | 16 |
| 3 | 24 | 8192 | 341 | 8 | 29.24% | 8 |
| 4 | 32 | 8192 | 256 | 0 | 21.88% | 32 |
| 5 | 48 | 8192 | 170 | 32 | 31.52% | 16 |
| ... | ... | ... | ... | ... | ... | ... |
| 64 | 24576 | 24576 | 1 | 0 | 11.45% | 8192 |
| 65 | 27264 | 81920 | 3 | 128 | 10.00% | 128 |
| 66 | 28672 | 57344 | 2 | 0 | 4.91% | 4096 |
| 67 | 32768 | 32768 | 1 | 0 | 12.50% | 8192 |
(1). class : mspan 等级表示,1-67
(2). bytes/obj : 每个对象的大小
(3). bytes/span : 该等级的 mspan 的总大小
(4). objects : 该等级的 mspan 最多可以 new 多少个对象,结果等于 (3) / (2)
(5). tail waste : 尾部浪费,当一个 mspan 中存放的对象大小不一致时,尾部剩余空间浪费的大小,结果等于 (3) % (2)
(6). max waste : 最大浪费空间,当一个 mspan 中存放的对象大小一致时,最大浪费空间
(7). min align : 最小对齐大小
除了上面根据大小确定的 mspan 等级外,每个 object 还有一个重要的属性叫做 noscan, 标识了 object 是否包含指针,在 gc 时是否需要展开标记
在 Golang 中,会将 span class + nocan 两部分信息组装成一个 uint8,形成完整的 spanClass 标识。8 个 bit 中,高 7 位表示了上表的 span 等级(总共 67+1 等级,8 个 bit 足够用了),最低位表示 noscan 信息。
代码位于 runtime/mheap.go
type spanClass uint8 | |
const ( | |
numSpanClasses = _NumSizeClasses << 1 | |
tinySpanClass = spanClass(tinySizeClass<<1 | 1) | |
) | |
//uint8 类型,高 7 位表示 span 等级,低 1 位表示 noscan | |
func makeSpanClass(sizeclass uint8, noscan bool) spanClass { | |
return spanClass(sizeclass<<1) | spanClass(bool2int(noscan)) | |
} | |
//go:nosplit | |
func (sc spanClass) sizeclass() int8 { | |
return int8(sc >> 1) | |
} | |
//go:nosplit | |
func (sc spanClass) noscan() bool { | |
return sc&1 != 0 | |
} |
# 线程缓存 mcache
const numSpanClasses = _NumSizeClasses << 1 = 136 | |
type mcache struct { | |
nextSample uintptr // trigger heap sample after allocating this many bytes | |
scanAlloc uintptr // bytes of scannable heap allocated | |
// 微对象分配器相关 | |
tiny uintptr | |
tinyoffset uintptr | |
tinyAllocs uintptr | |
//mcache 中缓存的 mspan,每种 spanClass 各一个 | |
alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass | |
stackcache [_NumStackOrders]stackfreelist | |
flushGen atomic.Uint32 | |
} |
- mcache 是每个 P 独有的缓存,因此交互无锁
- mcache 将每种 spanClass 等级的 mspan 各缓存了一个,总数为 2(noscan 维度) * 68(大小维度) = 136
- mcache 中还有一个为对象分配其 tiny allocator,用于处理小于 16B 对象的内存分配
# 中心缓存 mcentral
type mcentral struct { | |
// 对应的 spanClass | |
spanclass spanClass | |
// 有空位的 mspan 集合,数组长度为 2 适用于抗一轮 GC | |
partial [2]spanSet // list of spans with a free object | |
// 没有空位的 mspan 集合 | |
full [2]spanSet // list of spans with no free objects | |
} |
- 每个 mcentral 对应一种 spanClass
- 每个 mcentral 下聚合了该 spanClass 下的 mspan
- mcentral 下的 mspan 分为两个链表,分别位有空间 mspan 链表 partial 和 满空间 mspan 链表 full
- 每个 mcentral 一把锁
# 全局堆缓存 mheap
- 对于 Golang 上层应用而言,堆是操作系统虚拟内存的抽象
- 以页(8KB)为单位,作为最小内存存储单元
- 负责将连续页组装成 mspan
- 全局内存基于 bitMap 标识其使用情况,每个 bit 对应一页,为 0 则自由,为 1 则已被 mspan 组装
- 通过 heapArena 聚合页,记录了 页 到
mspan的映射信息 - 建立空闲页基数树索引 radix tree index,辅助快速寻找空闲页
- 是 mcentral 的持有者,持有所有 spanClass 下的 mcentral,作为自身的缓存
- 内存不够时,向操作系统申请,申请单位为 heapArena,大小为 64MB
type mheap struct { | |
// 堆的全局锁 | |
lock mutex | |
// 空闲页分配器,底层是多棵基数树组成的索引,每棵树对应 16GB 的空间 | |
pages pageAlloc // page allocation data structure | |
sweepgen uint32 // sweep generation, see comment in mspan; written during STW | |
// 记录了所有的 msapn,所有 mspan 都是经由 mheap,使用连续空闲页组装生成的 | |
allspans []*mspan // all spans out there | |
pagesInUse atomic.Uintptr // pages of spans in stats mSpanInUse | |
pagesSwept atomic.Uint64 // pages swept this cycle | |
pagesSweptBasis atomic.Uint64 // pagesSwept to use as the origin of the sweep ratio | |
sweepHeapLiveBasis uint64 // value of gcController.heapLive to use as the origin of sweep ratio; written with lock, read without | |
sweepPagesPerByte float64 // proportional sweep ratio; written with lock, read without | |
reclaimIndex atomic.Uint64 | |
reclaimCredit atomic.Uintptr | |
_ cpu.CacheLinePad // prevents false-sharing between arenas and preceding variables | |
//heapAreana 数组,64 位系统下,二维数组容量为 [1][2^22] | |
// 每个 heapArena 大小 64M,因此理论上,Golang 堆上限为 2^22 * 64M = 256TB | |
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena | |
arenasHugePages bool | |
heapArenaAlloc linearAlloc | |
arenaHints *arenaHint | |
arena linearAlloc | |
allArenas []arenaIdx | |
sweepArenas []arenaIdx | |
markArenas []arenaIdx | |
curArena struct { | |
base, end uintptr | |
} | |
// 多个 mecntral,总个数为 spanClass 的个数 | |
central [numSpanClasses]struct { | |
mcentral mcentral | |
// 用于内存地址对齐 | |
pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte | |
} | |
spanalloc fixalloc // allocator for span* | |
cachealloc fixalloc // allocator for mcache* | |
specialfinalizeralloc fixalloc // allocator for specialfinalizer* | |
specialprofilealloc fixalloc // allocator for specialprofile* | |
specialReachableAlloc fixalloc // allocator for specialReachable | |
specialPinCounterAlloc fixalloc // allocator for specialPinCounter | |
speciallock mutex // lock for special record allocators. | |
arenaHintAlloc fixalloc // allocator for arenaHints | |
userArena struct { | |
arenaHints *arenaHint | |
quarantineList mSpanList | |
readyList mSpanList | |
} | |
unused *specialfinalizer | |
} |
# 空闲页索引 pageAlloc
基数树作者原文
底层是多棵基数树组成的索引,每棵树对应 16GB 的空间
要理清这可基数树,首先需要明白一下几点:
- 数据结构背后的含义
mheap 会基于 bitMap 标识内存中各页的使用情况,bit 位为 0 代表该页是空闲的,为 1 代表已被使用
每棵基数树聚合了 16GB 内存空间中各页使用情况的索引信息,用于帮助 mheap 快速找到指定长度的连续空闲页的所在位置
mheap 持有 2^14 棵基数树,因此索引全面覆盖到 2^14 * 16GB = 256TB 的内存空间 - 基数树节点设定
基数树中,每个节点称之为 PallocSum, 是一个 uint64 类型,体现了索引的聚合信息,包含以下四部分:- start: 最右侧 21 个 bit,标识了当前节点映射的 bitMap 范围中首端有多少个连续的 0bit (空闲页),称之为 start
- max: 中间 21 个 bit,标识了当前节点映射的 bitMap 范围中最多有多少个连续的 0bit (空闲页),称之为 max
- end: 左侧 21 个 bit,标识了当前节点映射的 bitMap 范围中末端有多少个连续的 0bit (空闲页),称之为 end
- 最左侧一个 bit,弃之不用
- 父子关系
- 每个父 pallocSum 有 8 个子 pallocSum
- 根 pallocSum 总览全局,映射的 bitMap 范围为全局的 16GB (其 max 最大值为 2^21, 因此总空间大小为 2^21 * 8KB = 16GB)
- 从首层向下时一个依次八等分的过程,每一个 pallocSum 映射其父节点 bitMap 范围的 1/8,因此第二层 pallocSum 的 bitMap 范围为 16GB/8 = 2GB,以此类推,第 5 层节点的范围为 16GB / (8^4) = 4MB, 已经很小
- 聚合信息时,自底向上,每个父 pallocSum 聚合其所有子 pallocSum 的信息,因此根 pallocSum 聚合了全局的内存使用情况。每个父 pallocSum 聚合 8 个子 pallocSum 的 start、max、end 信息,因此根 pallocSum 的 start、max、end 信息分别代表了全局的起始空闲页、最大空闲页、末尾空闲页
- mheap 寻页时,自顶向下。对于遍历到的每个 pallocSum,先看起 start 是否符合,是则寻页成功;再看 max 是否符合,是则进入其下层孩子 pallocSum 中进一步寻访;最后看 end 和 下一个同辈 pallocSum 的 start 聚合后是否满足,是则寻页成功。
代码位于 runtime/mpagealloc.go 中:
type pageAlloc struct { | |
// 共有五层基数树,第一层有 2^14 个节点,因此共用 2^14 棵基数树 | |
// 总空间大小为 2^14 * 16GB = 256TB | |
// 接下来每层的节点数为上层的 8 倍 | |
summary [summaryLevels][]pallocSum | |
summaries and | |
chunks [1 << pallocChunksL1Bits]*[1 << pallocChunksL2Bits]pallocData | |
searchAddr offAddr | |
// start and end represent the chunk indices | |
// which pageAlloc knows about. It assumes | |
// chunks in the range [start, end) are | |
// currently ready to use. | |
start, end chunkIdx | |
// inUse is a slice of ranges of address space which are | |
// known by the page allocator to be currently in-use (passed | |
// to grow). | |
// | |
// We care much more about having a contiguous heap in these cases | |
// and take additional measures to ensure that, so in nearly all | |
// cases this should have just 1 element. | |
// | |
// All access is protected by the mheapLock. | |
inUse addrRanges | |
// scav stores the scavenger state. | |
scav struct { | |
// index is an efficient index of chunks that have pages available to | |
// scavenge. | |
index scavengeIndex | |
// releasedBg is the amount of memory released in the background this | |
// scavenge cycle. | |
releasedBg atomic.Uintptr | |
// releasedEager is the amount of memory released eagerly this scavenge | |
// cycle. | |
releasedEager atomic.Uintptr | |
} | |
// mheap_.lock. This level of indirection makes it possible | |
// to test pageAlloc independently of the runtime allocator. | |
mheapLock *mutex | |
// sysStat is the runtime memstat to update when new system | |
// memory is committed by the pageAlloc for allocation metadata. | |
sysStat *sysMemStat | |
// summaryMappedReady is the number of bytes mapped in the Ready state | |
// in the summary structure. Used only for testing currently. | |
// | |
// Protected by mheapLock. | |
summaryMappedReady uintptr | |
// chunkHugePages indicates whether page bitmap chunks should be backed | |
// by huge pages. | |
chunkHugePages bool | |
// Whether or not this struct is being used in tests. | |
test bool | |
} |
基数树节点
const( | |
logMaxPackedValue = 21 | |
maxPackedValue = 1 << logMaxPackedValue | |
) | |
type pallocSum uint64 | |
// 基于 start、max、end 组装成一个基数树节点 pallocSum | |
func packPallocSum(start, max, end uint) pallocSum { | |
if max == maxPackedValue { | |
return pallocSum(uint64(1 << 63)) | |
} | |
return pallocSum((uint64(start) & (maxPackedValue - 1)) | | |
((uint64(max) & (maxPackedValue - 1)) << logMaxPackedValue) | | |
((uint64(end) & (maxPackedValue - 1)) << (2 * logMaxPackedValue))) | |
} | |
// start extracts the start value from a packed sum. | |
func (p pallocSum) start() uint { | |
if uint64(p)&uint64(1<<63) != 0 { | |
return maxPackedValue | |
} | |
return uint(uint64(p) & (maxPackedValue - 1)) | |
} | |
// max extracts the max value from a packed sum. | |
func (p pallocSum) max() uint { | |
if uint64(p)&uint64(1<<63) != 0 { | |
return maxPackedValue | |
} | |
return uint((uint64(p) >> logMaxPackedValue) & (maxPackedValue - 1)) | |
} | |
// end extracts the end value from a packed sum. | |
func (p pallocSum) end() uint { | |
if uint64(p)&uint64(1<<63) != 0 { | |
return maxPackedValue | |
} | |
return uint((uint64(p) >> (2 * logMaxPackedValue)) & (maxPackedValue - 1)) | |
} | |
// unpack unpacks all three values from the summary. | |
func (p pallocSum) unpack() (uint, uint, uint) { | |
if uint64(p)&uint64(1<<63) != 0 { | |
return maxPackedValue, maxPackedValue, maxPackedValue | |
} | |
return uint(uint64(p) & (maxPackedValue - 1)), | |
uint((uint64(p) >> logMaxPackedValue) & (maxPackedValue - 1)), | |
uint((uint64(p) >> (2 * logMaxPackedValue)) & (maxPackedValue - 1)) | |
} |
# heapArena
- 每个 heapArena 包含 8192 个 page, 大小为 8192 * 8KB = 64MB
- heapArena 记录了页到 mspan 的映射。因为 GC 时,通过地址偏移找到到页很方便,但找到其所属的 mspan 不容易。因此需要通过这个映射信息进行辅助
- heapArena 是 mheap 向操作系统申请内存的单位 (64MB)
代码位于 runtime/mheap.go 中:
type heapArena struct { | |
heapArenaPtrScalar | |
// 实现 page 到 mspan 的映射 | |
spans [pagesPerArena]*mspan | |
pageInUse [pagesPerArena / 8]uint8 | |
// pageMarks is a bitmap that indicates which spans have any | |
// marked objects on them. Like pageInUse, only the bit | |
// corresponding to the first page in each span is used. | |
// | |
// Writes are done atomically during marking. Reads are | |
// non-atomic and lock-free since they only occur during | |
// sweeping (and hence never race with writes). | |
// | |
// This is used to quickly find whole spans that can be freed. | |
// | |
// TODO(austin): It would be nice if this was uint64 for | |
// faster scanning, but we don't have 64-bit atomic bit | |
// operations. | |
pageMarks [pagesPerArena / 8]uint8 | |
// pageSpecials is a bitmap that indicates which spans have | |
// specials (finalizers or other). Like pageInUse, only the bit | |
// corresponding to the first page in each span is used. | |
// | |
// Writes are done atomically whenever a special is added to | |
// a span and whenever the last special is removed from a span. | |
// Reads are done atomically to find spans containing specials | |
// during marking. | |
pageSpecials [pagesPerArena / 8]uint8 | |
// checkmarks stores the debug.gccheckmark state. It is only | |
// used if debug.gccheckmark > 0. | |
checkmarks *checkmarksMap | |
// zeroedBase marks the first byte of the first page in this | |
// arena which hasn't been used yet and is therefore already | |
// zero. zeroedBase is relative to the arena base. | |
// Increases monotonically until it hits heapArenaBytes. | |
// | |
// This field is sufficient to determine if an allocation | |
// needs to be zeroed because the page allocator follows an | |
// address-ordered first-fit policy. | |
// | |
// Read atomically and written with an atomic CAS. | |
zeroedBase uintptr | |
} |
# 对象分配流程
不管是以下哪种方式,最终都会殊途同归进入到 mallocgc 方法中
- new(T)
- &T{}
- make(xxx)
# 分配流程总览
goalng 中,依据 object 的大小,会将其分为下述三类:
- tiny 微对象(0-16KB)
- small 小对象(16KB-32KB)
- large 大对象(32KB 以上)
不同类型的对象,会有着不同的分配策略,这些内容在 mallocgc 方法中都有体现
核心流程类似于读多级缓存的过程,由上而下,每一步只要成功则直接返回。若失败,则由下层方法兜底
对于微对象的分配流程:
- 从 P 专属 mcache 的 tiny 分配器取内存 (无锁)
- 根据所属的 spanClass, 从 P 专属的 mcache 缓存的 mspan 中取内存(无锁)
- 根据所属的 spanclass 从对应的 mcentral 中取 mspan 填充到 mcache,然后从 mspan 填充到 mcache,然后从 mspan 中取内存(spanClass 粒度锁)
- 根据所属的 spanclass,从 mheap 的页分配器 pageAlloc 取得足够数量空闲页组装成 mspan,然后填充到 mcache,最后从 mspan 中取内存(mheap 粒度锁)
- mheap 向操作系统申请内存,更新页分配器的索引信息,然后重复 4
对于小对象的分配流程是跳过 1 步,执行上述流程的 2-5 步
对于大对象的分配流程是跳过 1-3 步,执行上述流程的 4-5 步
# 主干方法 mallocgc
# tiny 分配
每个 P 独有的 mcache 中会有一个 tiny 分配器,用于分配微对象,基于 offset 线性移动的方式对微对象进行分配,每 16B 成块,对象依据其大小,会向上取整为 2 的整数次幂进行空间补齐,然后进入分配流程。
noscan := typ == nil || typ.ptrdata == 0 | |
// ... | |
if noscan && size < maxTinySize { | |
//tiny 内存块中,从 offset 往后有空闲位置 | |
off := c.tinyoffset | |
// ... | |
// 如果当前 tiny 内存块空间还够用,则直接分配并返回 | |
if off+size <= maxTinySize && c.tiny != 0 { | |
// 分配空间 | |
x = unsafe.Pointer(c.tiny + off) | |
c.tinyoffset = off + size | |
c.tinyAllocs++ | |
mp.mallocing = 0 | |
releasem(mp) | |
return x | |
} | |
// ... | |
} |
# mcache 分配
var sizeclass uint8 | |
if size <= smallSizeMax-8 { | |
sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)] | |
} else { | |
sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)] | |
} | |
size = uintptr(class_to_size[sizeclass]) | |
spc := makeSpanClass(sizeclass, noscan) | |
span = c.alloc[spc] | |
v := nextFreeFast(span) | |
if v == 0 { | |
v, span, shouldhelpgc = c.nextFree(spc) | |
} | |
x = unsafe.Pointer(v) |
在 mspan 中,基于 Ctz64 算法,根据 mspan.allocCache 的 bitMap 信息快速检索到空闲的 object 块,进行返回。
代码位于 runtime/malloc.go 中
func nextFreeFast(s *mspan) gclinkptr { | |
// 通过 ctz64 算法,在 bit map 上寻找到首个 object 空位 | |
theBit := sys.Ctz64(s.allocCache) | |
if theBit < 64 { | |
result := s.freeindex + uintptr(theBit) | |
if result < s.nelems { | |
freeidx := result + 1 | |
if freeidx%64 == 0 && freeidx != s.nelems { | |
return 0 | |
} | |
s.allocCache >>= uint(theBit + 1) | |
// 偏移 freeindex | |
s.freeindex = freeidx | |
s.allocCount++ | |
// 返回获取 object 空位的内存地址 | |
return gclinkptr(result*s.elemsize + s.base()) | |
} | |
} | |
return 0 | |
} |
# mcentral 分配
当 mspan 无可用的 object 内存块时,会进入 mcache.nextFree 方法进行兜底。
代码位于 runtime/mcache.go 中
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) { | |
s = c.alloc[spc] | |
shouldhelpgc = false | |
freeIndex := s.nextFreeIndex() | |
if freeIndex == s.nelems { | |
// The span is full. | |
if s.allocCount != s.nelems { | |
println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems) | |
throw("s.allocCount != s.nelems && freeIndex == s.nelems") | |
} | |
c.refill(spc) | |
shouldhelpgc = true | |
s = c.alloc[spc] | |
freeIndex = s.nextFreeIndex() | |
} | |
if freeIndex >= s.nelems { | |
throw("freeIndex is not valid") | |
} | |
v = gclinkptr(uintptr(freeIndex)*s.elemsize + s.base()) | |
s.allocCount++ | |
if s.allocCount > s.nelems { | |
println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems) | |
throw("s.allocCount > s.nelems") | |
} | |
return | |
} |
倘若 mcache 中的 mspan 依然没有可用的内存块,则进入 mcache.refill 方法,从 mcentral 中获取 mspan 填充到 mcache 中,然后从 mspan 中获取内存块。
代码位于 runtime/mcache.go 中
func (c *mcache) refill(spc spanClass) { | |
s := c.alloc[spc] | |
// ... | |
// 从 mcentral 当中获取对应等级的 span | |
s = mheap_.central[spc].mcentral.cacheSpan() | |
// ... | |
// 将新的 span 添加到 mcahe 当中 | |
c.alloc[spc] = s | |
} |
mcentral.cacheSpan 方法中,会加锁(spanClass 级别的 sweepLocker),分别从 partial 和 full 中尝试获取有空间的 mspan:
代码位于 runtime/mcentral.go 文件中:
func (c *mcentral) cacheSpan() *mspan { | |
// ... | |
var sl sweepLocker | |
// ... | |
sl = sweep.active.begin() | |
if sl.valid { | |
for ; spanBudget >= 0; spanBudget-- { | |
s = c.partialUnswept(sg).pop() | |
// ... | |
if s, ok := sl.tryAcquire(s); ok { | |
// ... | |
sweep.active.end(sl) | |
goto havespan | |
} | |
// 通过 sweepLock,加锁尝试从 mcentral 的非空链表 full 中获取 mspan | |
for ; spanBudget >= 0; spanBudget-- { | |
s = c.fullUnswept(sg).pop() | |
// ... | |
if s, ok := sl.tryAcquire(s); ok { | |
// ... | |
sweep.active.end(sl) | |
goto havespan | |
} | |
// ... | |
} | |
} | |
// ... | |
} | |
// ... | |
// 执行到此处时,s 已经指向一个存在 object 空位的 mspan 了 | |
havespan: | |
// ... | |
return | |
} |
# mheap 分配
在 mcentral.cacheSpan 方法中,倘若从 partial 和 full 中都找不到合适的 mspan 了,则会调用 mcentral 的 grow 方法,将事态继续升级:
func (c *mcentral) cacheSpan() *mspan { | |
// ... | |
//mcentral 中也没有可用的 mspan 了,则需要从 mheap 中获取,最终会调用 mheap_.alloc 方法 | |
s = c.grow() | |
// ... | |
// 执行到此处时,s 已经指向一个存在 object 空位的 mspan 了 | |
havespan: | |
// ... | |
return | |
} |
经由 mcentral.grow 方法和 mheap.alloc 方法的周转,最终会步入 mheap.allocSpan 方法中:
func (c *mcentral) grow() *mspan { | |
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) | |
size := uintptr(class_to_size[c.spanclass.sizeclass()]) | |
s := mheap_.alloc(npages, c.spanclass) | |
// ... | |
// ... | |
return s | |
} |
代码位于 runtime/mheap.go 文件中:
func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan { | |
var s *mspan | |
systemstack(func() { | |
// ... | |
s = h.allocSpan(npages, spanAllocHeap, spanclass) | |
}) | |
return s | |
} | |
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) { | |
gp := getg() | |
base, scav := uintptr(0), uintptr(0) | |
//... 此处实际上还有一阶缓存,是从每个 P 的页缓存 pageCache 中获取空闲页组装 mspan,此处先略去了... | |
// 加上堆全局锁 | |
lock(&h.lock) | |
if base == 0 { | |
// 通过基数树索引快速寻找满足条件的连续空闲页 | |
base, scav = h.pages.alloc(npages) | |
// ... | |
} | |
// ... | |
unlock(&h.lock) | |
HaveSpan: | |
// 把空闲页组装成 mspan | |
s.init(base, npages) | |
// 将这批页添加到 heapArena 中,建立由页指向 mspan 的映射 | |
h.setSpans(s.base(), npages, s) | |
// ... | |
return s | |
} |
# 向操作系统申请
倘若 mheap 中没有足够多的空闲页了,会发起 mmap 系统调用,向操作系统申请额外的内存空间.
代码位于 runtime/mheap.go 文件的 mheap.grow 方法中:
func (h *mheap) grow(npage uintptr) (uintptr, bool) { | |
av, asize := h.sysAlloc(ask) | |
} | |
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { | |
v = sysReserve(unsafe.Pointer(p), n) | |
} | |
func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer { | |
return sysReserveOS(v, n) | |
} | |
func sysReserveOS(v unsafe.Pointer, n uintptr) unsafe.Pointer { | |
p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0) | |
if err != 0 { | |
return nil | |
} | |
return p | |
} |