大对象(large object)(>32kb)直接从 Go 堆上进行分配,不涉及 mcache/mcentral/mheap 之间的三级过程,也就相对简单。

从堆上分配

// 大对象分配
var s mspan
(…)
systemstack(func() {
s = largeAlloc(size, needzero, noscan)
})
s.freeindex = 1
s.allocCount = 1
x = unsafe.Pointer(s.base())
size = s.elemsize
可以看到,大对象所分配的 mspan 是直接通过 largeAlloc 进行分配的。
func largeAlloc(size uintptr, needzero bool, noscan bool)
mspan {
// 对象太大,溢出
if size+_PageSize < size {
throw(“out of memory”)
}

// 根据分配的大小计算需要分配的页数
npages := size >> _PageShift
if size&_PageMask != 0 {
npages++
}

  1. (...)

// 从堆上分配
s := mheap_.alloc(npages, makeSpanClass(0, noscan), true, needzero)
if s == nil {
throw(“out of memory”)
}
s.limit = s.base() + size

(…)

return s
}
从堆上分配调用了 alloc 方法,这个方法需要指明要分配的页数、span 的大小等级、是否为大对象、是否清零:
func (h mheap) alloc(npage uintptr, spanclass spanClass, large bool, needzero bool) mspan {
var s *mspan
systemstack(func() {
s = h.alloc_m(npage, spanclass, large)
})

if s != nil {
// 需要清零时,对分配的 span 进行清零
if needzero && s.needzero != 0 {
memclrNoHeapPointers(unsafe.Pointer(s.base()), s.npages<<_PageShift)
}
// 标记已经清零
s.needzero = 0
}
return s
}
alloc_m 是实际实现,在系统栈上执行:
//go:systemstack
func (h mheap) alloc_m(npage uintptr, spanclass spanClass, large bool) mspan {
g := getg()

(…)

lock(&h.lock)<br />    (...)<br />    _g_.m.mcache.local_scan = 0<br />    (...)<br />_g_.m.mcache.local_tinyallocs = 0

s := h.allocSpanLocked(npage, &memstats.heap_inuse)
if s != nil {
(…)
s.state = mSpanInUse
s.allocCount = 0
s.spanclass = spanclass
if sizeclass := spanclass.sizeclass(); sizeclass == 0 {
s.elemsize = s.npages << _PageShift
s.divShift = 0
s.divMul = 0
s.divShift2 = 0
s.baseMask = 0
} else {
s.elemsize = uintptr(class_to_size[sizeclass])
m := &class_to_divmagic[sizeclass]
s.divShift = m.shift
s.divMul = m.mul
s.divShift2 = m.shift2
s.baseMask = m.baseMask
}

 // Mark in-use span in arena page bitmap.<br />     arena, pageIdx, pageMask := pageIndexOf(s.base())<br />     arena.pageInUse[pageIdx] |= pageMask

 // update stats, sweep lists<br />     h.pagesInUse += uint64(npage)<br />     if large {<br />   (...)<br />   mheap_.largealloc += uint64(s.elemsize)<br />   mheap_.nlargealloc++<br />   (...)<br />     }<br /> }<br /> (...)<br />unlock(&h.lock)<br />return s<br />}<br />`allocSpanlocked` 用来从堆上根据页数来进行实际的分配工作:<br />func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan {<br />var s *mspan<br />    // 从堆中获取 span<br />s = h.pickFreeSpan(npage)<br />if s != nil {<br />     goto HaveSpan<br /> }<br />// 堆中没无法获取到 span,这时需要对堆进行增长<br />if !h.grow(npage) {<br />     return nil<br /> }<br />// 再获取一次<br />s = h.pickFreeSpan(npage)<br />if s != nil {<br />     goto HaveSpan<br /> }<br />throw("grew heap, but no adequate free span found")

HaveSpan:
(…)

if s.npages > npage {
t := (mspan)(h.spanalloc.alloc())
t.init(s.base()+npage<<_PageShift, s.npages-npage)
s.npages = npage
h.setSpan(t.base()-1, s)
h.setSpan(t.base(), t)
h.setSpan(t.base()+t.npages
pageSize-1, t)
t.needzero = s.needzero
start, end := t.physPageBounds()
if s.scavenged && start < end {
memstats.heap_released += uint64(end - start)
t.scavenged = true
}
s.state = mSpanManual
t.state = mSpanManual
h.freeSpanLocked(t, false, false, s.unusedsince)
s.state = mSpanFree
}
if s.scavenged {
sysUsed(unsafe.Pointer(s.base()), s.npages<<_PageShift)
s.scavenged = false
s.state = mSpanManual
h.scavengeLargest(s.npages * pageSize)
s.state = mSpanFree
}
s.unusedsince = 0

h.setSpans(s.base(), npage, s)
(…)
if s.inList() {
throw(“still in list”)
}
return s
}
从堆上获取 span 会同时检查 freescav 树堆:
func (h mheap) pickFreeSpan(npage uintptr) mspan {
tf := h.free.find(npage)
ts := h.scav.find(npage)
var s *mspan
// 选择更小的 span,然后返回
if tf != nil && (ts == nil || tf.spanKey.npages <= ts.spanKey.npages) {
s = tf.spanKey
h.free.removeNode(tf)
} else if ts != nil && (tf == nil || tf.spanKey.npages > ts.spanKey.npages) {
s = ts.spanKey
h.scav.removeNode(ts)
}
return s
}
free 和 scav 均为树堆,其数据结构的性质我们已经很熟悉了。

从操作系统申请

而对栈进行增长则需要向操作系统申请:
func (h *mheap) grow(npage uintptr) bool {
ask := npage << _PageShift
nBase := round(h.curArena.base+ask, physPageSize)
if nBase > h.curArena.end {
// Not enough room in the current arena. Allocate more
// arena space. This may not be contiguous with the
// current arena, so we have to request the full ask.
av, asize := h.sysAlloc(ask)
if av == nil {
print(“runtime: out of memory: cannot allocate “, ask, “-byte block (“, memstats.heap_sys, “ in use)\n”)
return false
}

 if uintptr(av) == h.curArena.end {<br />   // The new space is contiguous with the old<br />   // space, so just extend the current space.<br />   h.curArena.end = uintptr(av) + asize<br />     } else {<br />   // The new space is discontiguous. Track what<br />   // remains of the current space and switch to<br />   // the new space. This should be rare.<br />   if size := h.curArena.end - h.curArena.base; size != 0 {<br />       h.growAddSpan(unsafe.Pointer(h.curArena.base), size)<br />   }<br />   // Switch to the new space.<br />   h.curArena.base = uintptr(av)<br />   h.curArena.end = uintptr(av) + asize<br />     }<br />     // The memory just allocated counts as both released<br />     // and idle, even though it's not yet backed by spans.<br />     //<br />     // The allocation is always aligned to the heap arena<br />     // size which is always > physPageSize, so its safe to<br />     // just add directly to heap_released. Coalescing, if<br />     // possible, will also always be correct in terms of<br />     // accounting, because s.base() must be a physical<br />     // page boundary.<br />     memstats.heap_released += uint64(asize)<br />     memstats.heap_idle += uint64(asize)

 // Recalculate nBase<br />     nBase = round(h.curArena.base+ask, physPageSize)<br /> }

// Grow into the current arena.
v := h.curArena.base
h.curArena.base = nBase
h.growAddSpan(unsafe.Pointer(v), nBase-v)
return true
}

func (h *mheap) growAddSpan(v unsafe.Pointer, size uintptr) {
// Scavenge some pages to make up for the virtual memory space
// we just allocated, but only if we need to.
h.scavengeIfNeededLocked(size)

s := (mspan)(h.spanalloc.alloc())
s.init(uintptr(v), size/pageSize)
h.setSpans(s.base(), s.npages, s)
s.state = mSpanFree
// [v, v+size) is always in the Prepared state. The new span
// must be marked scavenged so the allocator transitions it to
// Ready when allocating from it.
s.scavenged = true
// This span is both released and idle, but grow already
// updated both memstats.
h.coalesce(s)
h.free.insert(s)
}
通过 h.sysAlloc 获取从操作系统申请而来的内存,首先尝试 从已经保留的 arena 中获得内存,无法获取到合适的内存后,才会正式向操作系统申请,而后对其进行初始化:
func (h
mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
n = round(n, heapArenaBytes)

// 优先从已经保留的 arena 中获取
v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys)
if v != nil {
size = n
goto mapped
}

// 如果获取不到,再尝试增长 arena hint
for h.arenaHints != nil {
hint := h.arenaHints
p := hint.addr
if hint.down {
p -= n
}
if p+n < p { // 溢出
v = nil
} else if arenaIndex(p+n-1) >= 1< v = nil
} else {
v = sysReserve(unsafe.Pointer(p), n)
}
if p == uintptr(v) {
// 获取成功,更新 arena hint
if !hint.down {
p += n
}
hint.addr = p
size = n
break
}
// 失败,丢弃并重新尝试
if v != nil {
sysFree(v, n, nil)
}
h.arenaHints = hint.next
h.arenaHintAlloc.free(unsafe.Pointer(hint))
}

if size == 0 {
(…)
v, size = sysReserveAligned(nil, n, heapArenaBytes)
if v == nil {
return nil, 0
}

 // 创建新的 hint 来增长此区域<br />     hint := (*arenaHint)(h.arenaHintAlloc.alloc())<br />     hint.addr, hint.down = uintptr(v), true<br />     hint.next, mheap_.arenaHints = mheap_.arenaHints, hint<br />     hint = (*arenaHint)(h.arenaHintAlloc.alloc())<br />     hint.addr = uintptr(v) + size<br />     hint.next, mheap_.arenaHints = mheap_.arenaHints, hint<br /> }

// 检查不能使用的指针
(…)

// 正式开始使用保留的内存
sysMap(v, size, &memstats.heap_sys)

mapped:
// 创建 arena 的 metadata
for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
l2 := h.arenas[ri.l1()]
if l2 == nil {
// 分配 L2 arena map
l2 = ([1 << arenaL2Bits]heapArena)(persistentalloc(unsafe.Sizeof(*l2), sys.PtrSize, nil))
if l2 == nil {
throw(“out of memory allocating heap arena map”)
}
(…)
}

 if l2[ri.l2()] != nil {<br />   throw("arena already initialized")<br />     }<br />     var r *heapArena<br />     r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))<br />     if r == nil {<br />   r = (*heapArena)(persistentalloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))<br />   if r == nil {<br />       throw("out of memory allocating heap arena metadata")<br />   }<br />     }

 // 将 arena 添加到 arena 列表中<br />     if len(h.allArenas) == cap(h.allArenas) {<br />   size := 2 * uintptr(cap(h.allArenas)) * sys.PtrSize<br />   if size == 0 {<br />       size = physPageSize<br />   }<br />   newArray := (*notInHeap)(persistentalloc(size, sys.PtrSize, &memstats.gc_sys))<br />   if newArray == nil {<br />       throw("out of memory allocating allArenas")<br />   }<br />   oldSlice := h.allArenas<br />   *(*notInHeapSlice)(unsafe.Pointer(&h.allArenas)) = notInHeapSlice{newArray, len(h.allArenas), int(size / sys.PtrSize)}<br />   copy(h.allArenas, oldSlice)<br />     }<br />     h.allArenas = h.allArenas[:len(h.allArenas)+1]<br />     h.allArenas[len(h.allArenas)-1] = ri

 (...)<br /> }

(…)

return
}
这个过程略显复杂:

  1. 首先会通过现有的 arena 中获得已经保留的内存区域,如果能获取到,则直接对 arena 进行初始化;
  2. 如果没有,则会通过 sysReserve 为 arena 保留新的内存区域,并通过 sysReserveAligned 对操作系统对齐的区域进行重排,而后使用 sysMap 正式使用所在区块的内存。
  3. 在 arena 初始化阶段,本质上是为 arena 创建 metadata,这部分内存属于堆外内存,即不会被 GC 所追踪的内存,因而通过 persistentalloc 进行分配。

persistentallocsysAlloc 之上的一层封装,它分配到的内存用于不能被释放。
func persistentalloc(size, align uintptr, sysStat uint64) unsafe.Pointer {
var p
notInHeap
systemstack(func() {
p = persistentalloc1(size, align, sysStat)
})
return unsafe.Pointer(p)
}
//go:systemstack
func persistentalloc1(size, align uintptr, sysStat uint64) notInHeap {
const (
maxBlock = 64 << 10 // VM reservation granularity is 64K on windows
)

// 不允许分配大小为 0 的空间
if size == 0 {
throw(“persistentalloc: size == 0”)
}
// 对齐数必须为 2 的指数、且不大于 PageSize
if align != 0 {
if align&(align-1) != 0 {
throw(“persistentalloc: align is not a power of 2”)
}
if align > _PageSize {
throw(“persistentalloc: align is too large”)
}
} else {
// 若未指定则默认为 8
align = 8
}

// 分配大内存:分配的大小如果超过最大的 block 大小,则直接调用 sysAlloc 进行分配
if size >= maxBlock {
return (*notInHeap)(sysAlloc(size, sysStat))
}

// 分配小内存:在 m 上进行
// 先获取 m
mp := acquirem()
var persistent persistentAlloc
if mp != nil && mp.p != 0 { // 如果能够获取到 m 且同时持有 p,则直接分配到 p 的 palloc 上
persistent = &mp.p.ptr().palloc
} else { // 否则就分配到全局的 globalAlloc.persistentAlloc 上
lock(&globalAlloc.mutex)
persistent = &globalAlloc.persistentAlloc
}
// 四舍五入 off 到 align 的倍数
persistent.off = round(persistent.off, align)
if persistent.off+size > persistentChunkSize || persistent.base == nil {
persistent.base = (
notInHeap)(sysAlloc(persistentChunkSize, &memstats.other_sys))
if persistent.base == nil {
if persistent == &globalAlloc.persistentAlloc {
unlock(&globalAlloc.mutex)
}
throw(“runtime: cannot allocate memory”)
}

 for {<br />   chunks := uintptr(unsafe.Pointer(persistentChunks))<br />   *(*uintptr)(unsafe.Pointer(persistent.base)) = chunks<br />   if atomic.Casuintptr((*uintptr)(unsafe.Pointer(&persistentChunks)), chunks, uintptr(unsafe.Pointer(persistent.base))) {<br />       break<br />   }<br />     }<br />     persistent.off = sys.PtrSize<br /> }<br />p := persistent.base.add(persistent.off)<br />persistent.off += size<br />releasem(mp)<br />if persistent == &globalAlloc.persistentAlloc {<br />     unlock(&globalAlloc.mutex)<br /> }

(…)
return p
}
可以看到,这里申请到的内存会被记录到 globalAlloc 中:
var globalAlloc struct {
mutex
persistentAlloc
}
type persistentAlloc struct {
base *notInHeap // 空结构,内存首地址
off uintptr // 偏移量
}