go 内存管理

操作系统内存管理

操作系统管理内存的存储单元是页(page),在 linux 中一般是 4KB。而且,操作系统还会使用 虚拟内存 来管理内存,在用户程序中,我们看到的内存是不是真实的内存,而是虚拟内存。当访问或者修改内存的时候,操作系统会将虚拟内存映射到真实的内存中。申请内存的组件是 Page Table 和 MMU(Memory Management Unit)。因为这个性能很重要,所以在 CPU 中专门有一个 TLB(Translation Lookaside Buffer)来缓存 Page Table 的内容。

为什么要用虚拟内存?

  1. 保护内存,每个进程都有自己的虚拟内存,不会相互干扰。防止修改和访问别的进程的内存。
  2. 减少内存碎片,虚拟内存是连续的,而真实的内存是不连续的。
  3. 当内存不够时,可以把虚拟内存映射到硬盘上,这样就可以使用硬盘的空间来扩展内存。

如上图所示,如果直接使用真实的内存,想要连续的内存肯定是申请不到的,这就是内存碎片的问题。而使用虚拟内存,通过 Page 映射的方式,保证内存连续。

Go 内存管理单元

page

在 go 中,管理内存的存储单元也是页(Page), 每个页的大小是 8KB。Go 内存管理是由 runtime 来管理的,runtime 会维护一个内存池,用来分配和回收内存。这样可以避免频繁的系统调用申请内存,提高性能。

mspan

mspan 是 go 内存管理基本单元,一个 mspan 包含一个或者多个 page。go 中有多种 mspan,每种 mspan 给不同的内存大小使用。

classbytes/objbytes/spanobjectstail wastemax wastemin align
1881921024087.50%8
2168192512043.75%16
3248192341829.24%8
4328192256021.88%32
54881921703231.52%16
6648192128023.44%64
78081921023219.07%16
8968192853215.95%32
91128192731613.56%16
6424576245761011.45%8192
652726481920312810.00%128
662867257344204.91%4096
6732768327681012.50%8192
  1. class 是 mspan 的类型,每种类型对应不同的内存大小。
  2. obj 是每个对象的大小。
  3. span 是 mspan 的大小。
  4. objects 是 mspan 中对象的个数。
  5. tail waste 是 mspan 中最后一个对象的浪费空间。(不能整除造成的)
  6. max waste 是 mspan 中最大的浪费空间。(比如第一个中 每个都使用 1 byte,那么就所有都浪费 7 byte,7 / 8 = 87.50%)
  7. min align 是 mspan 中对象的对齐大小。如果超过这个就会分配下一个 mspan。

数据结构

mspan

type mspan struct {
	// 双向链表 下一个 mspan 和 上一个 mspan
	next *mspan    
	prev *mspan    
  	// debug 使用的
	list *mSpanList 

  	// 起始地址和页数 当 class 太大 要多个页组成 mspan
	startAddr uintptr 
	npages    uintptr 

  	// 手动管理的空闲对象链表
	manualFreeList gclinkptr 

	// 下一个空闲对象的地址 如果小于它 就不用检索了 直接从这个地址开始 提高效率
	freeindex uint16
	// 对象的个数
	nelems uint16 
	// GC 扫描使用的空闲索引
	freeIndexForScan uint16

	// bitmap 每个 bit 对应一个对象 标记是否使用
	allocCache uint64

	// ...
  	// span 的类型 
	spanclass             spanClass     // size class and noscan (uint8)
	//  ...
}

spanClass

type spanClass uint8

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
}

spanClass 是 unint8 类型,一共有 8 位,前 7 位是 sizeclass,也就是上边 table 中的内容,一共有 (67 + 1) * 2 种类型, +1 是 0 代表比 67 class 的内存还大。最后一位是 noscan,也就是表示这个对象中是否含有指针,用来给 GC 扫描加速用的(无指针对象就不用继续扫描了),所以要 * 2。

mspan 详解

如果所示

  • mspan 是一个双向链表,如果不够用了,在挂一个就行了。
  • startAddr 是 mspan 的起始地址,npages 是 page 数量。根据 startAddr + npages * 8KB 就可以得到 mspan 的结束地址。
  • allocCache 是一个 bitmap,每个 bit 对应一个对象,标记是否使用。使用了 ctz(count trailing zero)。
  • freeindex 是下一个空闲对象的地址,如果小于它,就不用检索了,直接从这个地址开始,提高效率。

mcache

mache 是每个 P (processor)的结构体中都有的,是用来缓存的,因为每个 P 同一时间只有一个 goroutine 在执行,所以 mcache 是不需要加锁的。这也是 mcache 的设计初衷,减少锁的竞争,提高性能。

type p struct {
  // ...
  mcache      *mcache
  // ...
}
// 每个 P 的本队缓存
type mcache struct {
	// 不在 gc 的堆中分配
	_ sys.NotInHeap

	// The following members are accessed on every malloc,
	// so they are grouped here for better caching.
	nextSample uintptr // trigger heap sample after allocating this many bytes
	scanAlloc  uintptr // bytes of scannable heap allocated

	// 微对象分配器(<16B 不含指针)
	tiny       uintptr // 内存的其实地址
	tinyoffset uintptr // 偏移量
	tinyAllocs uintptr // 分配了多少个 tiny 对象


	// span缓存数组,按大小类别索引
	alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass

	// 用于不同大小的栈内存分配 go 的 堆上分配栈内存
	stackcache [_NumStackOrders]stackfreelist

	// 控制 mcache 的刷新
	flushGen atomic.Uint32
}

mcentral

mcentral 也是一种缓存,只不过在中心而不是在每个 P 上。mcentral 存在的意义也是减少锁竞争,如果没有 mcentral,那么只要从中心申请 mspan 就需要加锁。现在加上了 mcentral,申请时就需要加特别力度的锁就可以了,比如申请 class = 1 的 mspan 加 class = 1 的锁就可以了,不影响别人申请 class = 2 的 mspan。这样就可以较少锁竞争,提高性能。

type mcentral struct {
	_         sys.NotInHeap
	// mspan 的类别
	spanclass spanClass

	// 部分使用的span列表
	// 使用两个集合交替角色
	// [0] -> 已清扫的spans
	// [1] -> 未清扫的spans
	partial [2]spanSet // list of spans with a free object
	// 完全使用的 mspan
	full    [2]spanSet // list of spans with no free objects
}


type spanSet struct {
	// spanSet是一个两级数据结构,由一个可增长的主干(spine)指向固定大小的块组成。
	// 访问spine不需要锁,但添加块或扩展spine时需要获取spine锁。
	//
	// 因为每个mspan至少覆盖8K的堆内存,且在spanSet中最多占用8字节,
	// 所以spine的增长是相当有限的。

	// 锁
	spineLock mutex
	// 原子指针,指向一个动态数组
	spine     atomicSpanSetSpinePointer // *[N]atomic.Pointer[spanSetBlock]
	// 当前spine数组中实际使用的长度 原子类型
	spineLen  atomic.Uintptr            // Spine array length
	//  spine数组的容量
	spineCap  uintptr                   // Spine array cap, accessed under spineLock

	// index是spanSet中的头尾指针,被压缩在一个字段中。
	// head和tail都表示所有块的逻辑连接中的索引位置,其中head总是在tail之后或等于tail
	// (等于tail时表示集合为空)。这个字段始终通过原子操作访问。
	//
	// head和tail各自的宽度为32位,这意味着在需要重置之前,我们最多支持2^32次push操作。
	// 如果堆中的每个span都存储在这个集合中,且每个span都是最小尺寸(1个运行时页面,8 KiB),
	// 那么大约需要32 TiB大小的堆才会导致无法表示的情况。
	// 头部索引
	index atomicHeadTailIndex
}
type mheap struct {
	central [numSpanClasses]struct {
		mcentral mcentral
		// 填充字节 一般不能整除的时候 末尾的余数就不用了
		pad      [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
	}
}

mheap

mheap 是全局的内存管理器,申请内存是 mcentral 不满足要求的时候,就会从 mheap 中申请,要加全局锁。如果 mheap 还不能满足,就会系统调用从操作系统申请,每次申请的最小单位是 Arena,也就是 64M。

type mheap struct {
	_ sys.NotInHeap

	// 全局锁
	lock mutex
	// page 分配器 管理所有的page 
	pages pageAlloc 

	sweepgen uint32 // sweep 代数 gc时候使用

	// 所有的 mspan
	allspans []*mspan 

	// 正在使用的 page 数
	pagesInUse         atomic.Uintptr 
	// ......

	// 用于定位内存地址是哪个 mspan 的
	// 二维数组 1 << arenaL1Bits = 1   1 << arenaL2Bits = 4194304 
	arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena

	spanalloc fixalloc              // span 分配器
	cachealloc fixalloc             // mcache 分配器
	specialfinalizeralloc fixalloc  // finalizer 分配器
	// ......
}

heapArena


// A heapArena stores metadata for a heap arena. heapArenas are stored
// outside of the Go heap and accessed via the mheap_.arenas index.
type heapArena struct {
	_ sys.NotInHeap

	// page 对应的 mspan
	// pagesPerArena 8192 一个 page 8KB 所以一个 heapArena 可以存储 64M 的内存
	spans [pagesPerArena]*mspan

	// 标记哪个 page 是在使用的
	// /8 是 uint8 可以表示 8 个 page
	pageInUse [pagesPerArena / 8]uint8

	// 标记哪些span包含被标记的对象 用于 gc 加速
	pageMarks [pagesPerArena / 8]uint8

	// 标记哪些span包含特殊对象
	pageSpecials [pagesPerArena / 8]uint8

	checkmarks *checkmarksMap

	// arena中第一个未使用(已归零)页面的起始字节
	zeroedBase uintptr
}

pageAlloc

分配 page 的结构体,是一个 radix tree 的结构,一共有 5 层,每一层都是一个 summary 数组,用于快速查找空闲页面。

type pageAlloc struct {
	// 基数树 一共有 summaryLevels=5 层
	// 基数树的摘要数组,用于快速查找空闲页面
	summary [summaryLevels][]pallocSum

	//  二级页面位图结构 
	// 使用二级结构而不是一个大的扁平数组,是因为在64位平台上总大小可能非常大(O(GiB))
	chunks [1 << pallocChunksL1Bits]*[1 << pallocChunksL2Bits]pallocData

	// 搜索起始地址
	searchAddr offAddr

	// start 和 end 表示 pageAlloc 知道的块索引范围
	start, end chunkIdx

	// ......
}
type pallocSum uint64

//  pallocSum 被划分成几个部分:
// 63位     62-42位    41-21位    20-0位
// [标志位] [end值]    [max值]    [start值]
//  1      21位      21位       21位

func (p pallocSum) start() uint {
	// 检查第63位是否为1
	if uint64(p)&uint64(1<<63) != 0 {
		return maxPackedValue
	}
	// 否则,取最低21位
	return uint(uint64(p) & (maxPackedValue - 1))
}

func (p pallocSum) max() uint {
	if uint64(p)&uint64(1<<63) != 0 {
		return maxPackedValue
	}
	// 右移21位,然后取21位
	return uint((uint64(p) >> logMaxPackedValue) & (maxPackedValue - 1))
}

func (p pallocSum) end() uint {
	if uint64(p)&uint64(1<<63) != 0 {
		return maxPackedValue
	}
	// 右移42位,然后取21位
	return uint((uint64(p) >> (2 * logMaxPackedValue)) & (maxPackedValue - 1))
}

内存分配流程

流程

go 中把 对象分成三类 tiny ,small 和 large。tiny 是小于 16B 的对象,small 是大于等于 16B 小于 32KB 的对象,large 是大于 32KB 的对象。tiny 分配器主要是为了减少内存碎片。

  1. 如果是 tiny object,直接使用 tiny 分配器分配。如果 tiny 分配器中的空间不够(定长位16B),就从 mchunk 中获取一个新的 16B 的对象作为 tiny 分配器的空间使用。
  2. 如果是 small object,根据所属的 class, 从 mcache 获取对应 mspan 中的内存。
  3. 如果 mspan 中的内存不够,根据所属的 class 从 mcentral 中获取新的 mspan ,从 mspan 中获取内存。(要 class 力度的锁)
  4. 如果 mcentral 中的 mspan 也不够,就从 mheap 中获取对应数量的 page 组装成 mspan,然后从新的 mspan 中获取内存。(全局锁)
  5. 如果 mheap 中的 mspan 也不够,就系统调用从操作系统获取新的 Arena。把内存 page 分配好,然后继续第四步。
  6. 如果是 large object,直接从第四部开始。

mallocgc

// 在 heap 上分配内存函数 size 对象大小 typ 对象类型 needzero 是否需要清零
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	// gc 终止阶段不允许分配 这个是一个检查 正常情况下不会出现
	if gcphase == _GCmarktermination {
		throw("mallocgc called with gcphase == _GCmarktermination")
	}
	// 处理分类为 0 的情况
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}

	// ......


	// Set mp.mallocing to keep from being preempted by GC.
	mp := acquirem()
	if mp.mallocing != 0 {
		throw("malloc deadlock")
	}
	if mp.gsignal == getg() {
		throw("malloc during signal")
	}
	mp.mallocing = 1

	shouldhelpgc := false
	dataSize := userSize
	// 获取 M 和 M 所属 P 的 mcache
	c := getMCache(mp)
	if c == nil {
		throw("mallocgc called without a P or outside bootstrapping")
	}
	var span *mspan
	var header **_type
	var x unsafe.Pointer
	// 对象总是不是含有指针  如果不含有 就不用往下扫描了 用来 gc 加速
	noscan := typ == nil || !typ.Pointers()

	// 是不是小对象 (< 32k - 8)
	if size <= maxSmallSize-mallocHeaderSize {
		// 如果对象大小小于 16B 且不含有指针 则使用 tiny 分配器
		if noscan && size < maxTinySize {
			off := c.tinyoffset
			// 内存对齐一下
			if size&7 == 0 {
				off = alignUp(off, 8)
			} else if goarch.PtrSize == 4 && size == 12 {
				off = alignUp(off, 8)
			} else if size&3 == 0 {
				off = alignUp(off, 4)
			} else if size&1 == 0 {
				off = alignUp(off, 2)
			}
			// 如果剩余空间足够 使用 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
			}
			// 重新 分配一个 tiny 使用 
			span = c.alloc[tinySpanClass]
			v := nextFreeFast(span)
			if v == 0 {
				v, span, shouldhelpgc = c.nextFree(tinySpanClass)
			}
			x = unsafe.Pointer(v)
			(*[2]uint64)(x)[0] = 0
			(*[2]uint64)(x)[1] = 0
			if !raceenabled && (size < c.tinyoffset || c.tiny == 0) {
				c.tiny = uintptr(x)
				c.tinyoffset = size
			}
			size = maxTinySize
		} else {
			// 处理小对象
			// 处理对象头部 主要加入一些头部信息帮助 GC 加速
			hasHeader := !noscan && !heapBitsInSpan(size)
			if hasHeader {
				size += mallocHeaderSize
			}
			// 根据不同的对象大小 使用不同的mspan
			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]
			// 使用缓存从 mspan 中获取空闲对象
			v := nextFreeFast(span)
			if v == 0 {
				// 先从本地获取 span 如果本地没获取到 升级到 mcenral 获取
				v, span, shouldhelpgc = c.nextFree(spc)
			}
			x = unsafe.Pointer(v)
			// 如果需要清零 处理一下
			if needzero && span.needzero != 0 {
				memclrNoHeapPointers(x, size)
			}
			// 设置头
			if hasHeader {
				header = (**_type)(x)
				x = add(x, mallocHeaderSize)
				size -= mallocHeaderSize
			}
		}
	} else {
		// 大对象分配 直接从 mheap 中获取 class = 0 的 mspan
		shouldhelpgc = true
		span = c.allocLarge(size, noscan)
		span.freeindex = 1
		span.allocCount = 1
		size = span.elemsize
		x = unsafe.Pointer(span.base())
		if needzero && span.needzero != 0 {
			delayedZeroing = true
		}
		if !noscan {
			// Tell the GC not to look at this yet.
			span.largeType = nil
			header = &span.largeType
		}
	}
	// ......
	return x
}

nextFreeFast

func nextFreeFast(s *mspan) gclinkptr {
	// 使用 ctz64 (amd64 中是 tzcnt 指令 )  获取末尾的 0(以分配) 的个数 如果是 64 说明没有空闲对象
	theBit := sys.TrailingZeros64(s.allocCache) 
	// 如果找到了空闲位置(theBit < 64)
	if theBit < 64 {
		result := s.freeindex + uint16(theBit)
		if result < s.nelems {
			freeidx := result + 1
			if freeidx%64 == 0 && freeidx != s.nelems {
				return 0
			}
			// 分配了 cache 移动一下
			s.allocCache >>= uint(theBit + 1)
			s.freeindex = freeidx
			s.allocCount++
			// result * elemsize:计算对象的偏移量
            // base():获取span的起始地址
			return gclinkptr(uintptr(result)*s.elemsize + s.base())
		}
	}
	return 0
}

nextFree

// nextFree 从 mcache 中获取下一个空闲对象
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
	s = c.alloc[spc]
	shouldhelpgc = false
	// 从 mcache 对象空位的偏移量
	freeIndex := s.nextFreeIndex()
	if freeIndex == s.nelems {
		// mcache 没有靠你先对象 从 mcentral,mheap 获取
		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
}

一组一组获取空闲对象

func (s *mspan) nextFreeIndex() uint16 {
	sfreeindex := s.freeindex
	snelems := s.nelems
	if sfreeindex == snelems {
		return sfreeindex
	}
	if sfreeindex > snelems {
		throw("s.freeindex > s.nelems")
	}

	aCache := s.allocCache

	bitIndex := sys.TrailingZeros64(aCache)
	for bitIndex == 64 {
		// Move index to start of next cached bits.
		sfreeindex = (sfreeindex + 64) &^ (64 - 1)
		if sfreeindex >= snelems {
			s.freeindex = snelems
			return snelems
		}
		whichByte := sfreeindex / 8
		// Refill s.allocCache with the next 64 alloc bits.
		s.refillAllocCache(whichByte)
		aCache = s.allocCache
		bitIndex = sys.TrailingZeros64(aCache)
		// nothing available in cached bits
		// grab the next 8 bytes and try again.
	}
	result := sfreeindex + uint16(bitIndex)
	if result >= snelems {
		s.freeindex = snelems
		return snelems
	}

	s.allocCache >>= uint(bitIndex + 1)
	sfreeindex = result + 1

	if sfreeindex%64 == 0 && sfreeindex != snelems {
		whichByte := sfreeindex / 8
		s.refillAllocCache(whichByte)
	}
	s.freeindex = sfreeindex
	return result
}

// 给 mcache 添加一个新的 mspan 一般是申请内存 mcache 中没有空闲对象了
func (c *mcache) refill(spc spanClass) {
	s := c.alloc[spc]

	if s.allocCount != s.nelems {
		throw("refill of span with free space remaining")
	}
	if s != &emptymspan {
		// ......
		// 如果不是空的 而且没有空闲对象 就把这个 span 放到 mcentral 中 mcache 使用不了这个 span 了
		mheap_.central[spc].mcentral.uncacheSpan(s)

		// ......
	}

	// 从 mcentral 获取新的 span 如果没有就从 mheap 再没有就系统调用申请内存
	s = mheap_.central[spc].mcentral.cacheSpan()
	
	// .....

	c.alloc[spc] = s
}

cacheSpan

func (c *mcentral) cacheSpan() *mspan {
	// ......

	// 尝试从以清扫的部分获取
	sg := mheap_.sweepgen
	if s = c.partialSwept(sg).pop(); s != nil {
		goto havespan
	}

	// 如果以清扫的没有 就马上开始主动清扫
	sl = sweep.active.begin()
	if sl.valid {
		// 尝试从未清扫的部分使用的 span 列表中获取 
		for ; spanBudget >= 0; spanBudget-- {
			s = c.partialUnswept(sg).pop()
			if s == nil {
				break
			}
			// 尝试获取 span 
			if s, ok := sl.tryAcquire(s); ok {
				// 清扫它 并使用
				s.sweep(true)
				sweep.active.end(sl)
				goto havespan
			}
		}
		// 尝试从未清扫的已满 span 列表中获取
		for ; spanBudget >= 0; spanBudget-- {
			s = c.fullUnswept(sg).pop()
			if s == nil {
				break
			}
			if s, ok := sl.tryAcquire(s); ok {
				s.sweep(true)
				// 清扫之后 看有无可用的 没有就下一个
				freeIndex := s.nextFreeIndex()
				if freeIndex != s.nelems {
					s.freeindex = freeIndex
					sweep.active.end(sl)
					goto havespan
				}
				c.fullSwept(sg).push(s.mspan)
			}
		}
		sweep.active.end(sl)
	}
	trace = traceAcquire()
	if trace.ok() {
		trace.GCSweepDone()
		traceDone = true
		traceRelease(trace)
	}

	// mcentral 中没有可用的 span 了 从 mheap 中获取
	s = c.grow()
	if s == nil {
		return nil
	}

	// 获取到了 span 了 上边会 goto 到这
havespan:
	// ......
	// 处理 allocCache 缓存
	freeByteBase := s.freeindex &^ (64 - 1)
	whichByte := freeByteBase / 8
	s.refillAllocCache(whichByte)
	s.allocCache >>= s.freeindex % 64

	return s
}

grow

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)
	if s == nil {
		return nil
	}

	// 计算这个 span 可以容纳多少个对象 和 偏移量等
	n := s.divideByElemSize(npages << _PageShift)
	s.limit = s.base() + size*n
	s.initHeapBits(false)
	return s
}

func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {
	var s *mspan
	systemstack(func() {
		// 先清扫一些页 防止一直增长
		if !isSweepDone() {
			h.reclaim(npages)
		}
		s = h.allocSpan(npages, spanAllocHeap, spanclass)
	})
	return s
}

func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) {
	// 检查内存对其 ......
	if !needPhysPageAlign && pp != nil && npages < pageCachePages/4 {
		// 尝试从缓存直接获取
		*c = h.pages.allocToCache()
	}
	// 加锁
	lock(&h.lock)

	if needPhysPageAlign {
		// Overallocate by a physical page to allow for later alignment.
		extraPages := physPageSize / pageSize

		// 尝试从 pageAlloc 获取页
		base, _ = h.pages.find(npages + extraPages)
		
	}

	if base == 0 {
		// 尝试分配所需页数
		base, scav = h.pages.alloc(npages)
		if base == 0 {
			var ok bool
			// 空间不足,尝试扩展
			growth, ok = h.grow(npages)
			if !ok {
				unlock(&h.lock)
				return nil
			}
			base, scav = h.pages.alloc(npages)
			if base == 0 {
				throw("grew heap, but no adequate free space found")
			}
		}
	}
	unlock(&h.lock)

HaveSpan:
	// ......

	// 组装成 mspan
	h.initSpan(s, typ, spanclass, base, npages)

	return s
}
// 向操作系统申请内存
func (h *mheap) grow(npage uintptr) (uintptr, bool) {
	// 每次申请 4 M
	ask := alignUp(npage, pallocChunkPages) * pageSize
	// ......
	av, asize := h.sysAlloc(ask, &h.arenaHints, true)
	// ......
}

// sysAlloc -> sysReserve -> sysReserveOS
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
}
func mmap(addr unsafe.Pointer, n uintptr, prot, flags, fd int32, off uint32) (unsafe.Pointer, int) {
	// ......
	return sysMmap(addr, n, prot, flags, fd, off)
}

stack 内存

// newproc1
if newg == nil {
	newg = malg(stackMin)
}
//newproc1 -> malg -> stackalloc
func stackalloc(n uint32) stack {
	thisg := getg()
	// ......

	var v unsafe.Pointer
	// 小栈 linux 下是 32k
	if n < fixedStack<<_NumStackOrders && n < _StackCacheSize {
		order := uint8(0)
		n2 := n
		for n2 > fixedStack {
			order++
			n2 >>= 1
		}
		var x gclinkptr
		 // 以下情况直接从全局池分配:
        // 1. 禁用栈缓存
        // 2. 没有关联的 P
        // 3. 禁用抢占
		if stackNoCache != 0 || thisg.m.p == 0 || thisg.m.preemptoff != "" {
			lock(&stackpool[order].item.mu)
			x = stackpoolalloc(order)
			unlock(&stackpool[order].item.mu)
		} else {
			// 从 P 的本地缓存分配
			c := thisg.m.p.ptr().mcache
			x = c.stackcache[order].list
			// 如果本地缓存为空,则重新填充
			if x.ptr() == nil {
				stackcacherefill(c, order)
				x = c.stackcache[order].list
			}
			c.stackcache[order].list = x.ptr().next
			c.stackcache[order].size -= uintptr(n)
		}
		v = unsafe.Pointer(x)
	} else {
		// 大栈
		var s *mspan
		npage := uintptr(n) >> _PageShift
		log2npage := stacklog2(npage)

		// Try to get a stack from the large stack cache.
		lock(&stackLarge.lock)
		if !stackLarge.free[log2npage].isEmpty() {
			s = stackLarge.free[log2npage].first
			stackLarge.free[log2npage].remove(s)
		}
		unlock(&stackLarge.lock)

		lockWithRankMayAcquire(&mheap_.lock, lockRankMheap)

		if s == nil {
			// 从堆中分配新的栈空间
			s = mheap_.allocManual(npage, spanAllocStack)
			if s == nil {
				throw("out of memory")
			}
			osStackAlloc(s)
			s.elemsize = uintptr(n)
		}
		v = unsafe.Pointer(s.base())
	}

	// ...
	return stack{uintptr(v), uintptr(v) + uintptr(n)}
}

stackpoolalloc stackpool

var stackpool [_NumStackOrders]struct {
	item stackpoolItem
	_    [(cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}

type stackpoolItem struct {
	_    sys.NotInHeap
	mu   mutex
	span mSpanList
}


func stackpoolalloc(order uint8) gclinkptr {
	list := &stackpool[order].item.span
	s := list.first
	if s == nil {
		// 从 mheap 中申请 class = 0 对应页数的
		s = mheap_.allocManual(_StackCacheSize>>_PageShift, spanAllocStack)
		// ...
	}
	// 分配内存
	x := s.manualFreeList
	// ...
	return x
}

stackcache

type mcache struct {
	stackcache [_NumStackOrders]stackfreelist
}

type stackfreelist struct {
	list gclinkptr 
	size uintptr  
}

type gclinkptr uintptr

func (p gclinkptr) ptr() *gclink {
	return (*gclink)(unsafe.Pointer(p))
}

stackcacherefill

func stackcacherefill(c *mcache, order uint8) {
	for size < _StackCacheSize/2 {
		x := stackpoolalloc(order)
		x.ptr().next = list
		list = x
		size += fixedStack << order
	}
	unlock(&stackpool[order].item.mu)
	c.stackcache[order].list = list
	c.stackcache[order].size = size
}

回收

//Goexit -> goexit1 -> goexit0 -> gdestroy
func gdestroy(gp *g) {
	// ......
	// 修改状态
	casgstatus(gp, _Grunning, _Gdead)
	// 把 gp 的变量制空 .......

	// 把 m 上的 g 制空
	dropg()

	gfput(pp, gp)
}

func gfput(pp *p, gp *g) {
	// ......
	stksize := gp.stack.hi - gp.stack.lo

	// 如果栈不是默认大小 直接释放掉 只有默认大小才去复用
	if stksize != uintptr(startingStackSize) {
		// non-standard stack size - free it.
		stackfree(gp.stack)
		gp.stack.lo = 0
		gp.stack.hi = 0
		gp.stackguard0 = 0
	}

	// 将 goroutine 放入空闲队列
	pp.gFree.push(gp)
	pp.gFree.n++
	// 如果到达 64 个 goroutine 就把一部分放到全局队列中
	if pp.gFree.n >= 64 {
		var (
			inc      int32
			stackQ   gQueue
			noStackQ gQueue
		)
		for pp.gFree.n >= 32 {
			gp := pp.gFree.pop()
			pp.gFree.n--
			if gp.stack.lo == 0 {
				noStackQ.push(gp)
			} else {
				stackQ.push(gp)
			}
			inc++
		}
		lock(&sched.gFree.lock)
		sched.gFree.noStack.pushAll(noStackQ)
		sched.gFree.stack.pushAll(stackQ)
		sched.gFree.n += inc
		unlock(&sched.gFree.lock)
	}
}

stackfree

func stackfree(stk stack) {
	// ......
	
	if n < fixedStack<<_NumStackOrders && n < _StackCacheSize {
		// 小栈(< 32k) 留着复用一下
		order := uint8(0)
		n2 := n
		for n2 > fixedStack {
			order++
			n2 >>= 1
		}
		x := gclinkptr(v)
		 // 如果不使用缓存或当前处理器被抢占,使用全局栈池
		if stackNoCache != 0 || gp.m.p == 0 || gp.m.preemptoff != "" {
			lock(&stackpool[order].item.mu)
			stackpoolfree(x, order)
			unlock(&stackpool[order].item.mu)
		} else {
			// 否则,使用本地缓存
			c := gp.m.p.ptr().mcache
			if c.stackcache[order].size >= _StackCacheSize {
				stackcacherelease(c, order)
			}
			x.ptr().next = c.stackcache[order].list
			c.stackcache[order].list = x
			c.stackcache[order].size += n
		}
	} else {
		// 如果栈大小不适合缓存,检查其 span 状态并相应处理
		s := spanOfUnchecked(uintptr(v))
		if s.state.get() != mSpanManual {
			println(hex(s.base()), v)
			throw("bad span state")
		}
		if gcphase == _GCoff {
			// 如果 GC 未运行,立即释放栈
			osStackFree(s)
			mheap_.freeManual(s, spanAllocStack)
		} else {
			// 如果 GC 运行中,将栈添加到大栈缓存,避免与 GC 竞态
			log2npage := stacklog2(s.npages)
			lock(&stackLarge.lock)
			stackLarge.free[log2npage].insert(s)
			unlock(&stackLarge.lock)
		}
	}
}