原文地址

# 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
}
  1. mcache 是每个 P 独有的缓存,因此交互无锁
  2. mcache 将每种 spanClass 等级的 mspan 各缓存了一个,总数为 2(noscan 维度) * 68(大小维度) = 136
  3. 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 的空间
要理清这可基数树,首先需要明白一下几点:

  1. 数据结构背后的含义
    mheap 会基于 bitMap 标识内存中各页的使用情况,bit 位为 0 代表该页是空闲的,为 1 代表已被使用
    每棵基数树聚合了 16GB 内存空间中各页使用情况的索引信息,用于帮助 mheap 快速找到指定长度的连续空闲页的所在位置
    mheap 持有 2^14 棵基数树,因此索引全面覆盖到 2^14 * 16GB = 256TB 的内存空间
  2. 基数树节点设定
    基数树中,每个节点称之为 PallocSum, 是一个 uint64 类型,体现了索引的聚合信息,包含以下四部分:
    • start: 最右侧 21 个 bit,标识了当前节点映射的 bitMap 范围中首端有多少个连续的 0bit (空闲页),称之为 start
    • max: 中间 21 个 bit,标识了当前节点映射的 bitMap 范围中最多有多少个连续的 0bit (空闲页),称之为 max
    • end: 左侧 21 个 bit,标识了当前节点映射的 bitMap 范围中末端有多少个连续的 0bit (空闲页),称之为 end
    • 最左侧一个 bit,弃之不用
  3. 父子关系
    • 每个父 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 方法中都有体现

核心流程类似于读多级缓存的过程,由上而下,每一步只要成功则直接返回。若失败,则由下层方法兜底

对于微对象的分配流程:

  1. 从 P 专属 mcache 的 tiny 分配器取内存 (无锁)
  2. 根据所属的 spanClass, 从 P 专属的 mcache 缓存的 mspan 中取内存(无锁)
  3. 根据所属的 spanclass 从对应的 mcentral 中取 mspan 填充到 mcache,然后从 mspan 填充到 mcache,然后从 mspan 中取内存(spanClass 粒度锁)
  4. 根据所属的 spanclass,从 mheap 的页分配器 pageAlloc 取得足够数量空闲页组装成 mspan,然后填充到 mcache,最后从 mspan 中取内存(mheap 粒度锁)
  5. 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
}
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

ZJM 微信支付

微信支付

ZJM 支付宝

支付宝