funcsysReserve(v unsafe.Pointer, n uintptr, reserved * bool) unsafe.Pointer { *reserved = truep := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0)

ifuintptr(p) < 4096{

returnnil}

returnp}

//linux

funcsysReserve(v unsafe.Pointer, n uintptr, reserved * bool) unsafe.Pointer { ... p := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0)

ifuintptr(p) < 4096{

returnnil} *reserved = truereturnp}

//windows

funcsysReserve(v unsafe.Pointer, n uintptr, reserved * bool) unsafe.Pointer { *reserved = true// v is just a hint.// First try at v.v = unsafe.Pointer(stdcall4(_VirtualAlloc, uintptr(v), n, _MEM_RESERVE, _PAGE_READWRITE))

ifv != nil{

returnv } // Next let the kernel choose the address.returnunsafe.Pointer(stdcall4(_VirtualAlloc, 0, n, _MEM_RESERVE, _PAGE_READWRITE))} 3.3 mheap 初始化

我们上面介绍 mheap 结构的时候知道 spans, bitmap, arena 都是存在于 mheap 中的,从操作系统申请完地址之后就是初始化 mheap 了。

funcmallocinit() { ... p1 := round(p, _PageSize) spansStart := p1 mheap_.bitmap = p1 + spansSize + bitmapSize

ifsys.PtrSize == 4{

// Set arena_start such that we can accept memory// reservations located anywhere in the 4GB virtual space.mheap_.arena_start = 0} else{ mheap_.arena_start = p1 + (spansSize + bitmapSize) } mheap_.arena_end = p + pSize mheap_.arena_used = p1 + (spansSize + bitmapSize) mheap_.arena_reserved = reserved

ifmheap_.arena_start&(_PageSize -1) != 0{

println( "bad pagesize", hex(p), hex(p1), hex(spansSize), hex(bitmapSize), hex(_PageSize), "start", hex(mheap_.arena_start)) throw( "misrounded allocation in mallocinit") } // Initialize the rest of the allocator.mheap_.init(spansStart, spansSize) //获取当前 G_g_ := getg() // 获取 G 上绑定的 M 的 mcache_g_.m.mcache = allocmcache()}

p 是从连续虚拟地址的起始地址,先进行对齐,然后初始化 arena,bitmap,spans 地址。mheap_.init()会初始化 fixalloc 等相关的成员,还有 mcentral 的初始化。

func(h *mheap) init(spansStart, spansBytes uintptr) { h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys) h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys) h.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys) h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys) h.spanalloc.zero = false// h->mapcache needs no initfori := rangeh.free { h.free[i].init() h.busy[i].init() } h.freelarge.init() h.busylarge.init()

fori := rangeh.central { h.central[i].mcentral.init( int32(i)) } sp := (*slice)(unsafe.Pointer(&h.spans)) sp.array = unsafe.Pointer(spansStart) sp. len= 0sp. cap= int(spansBytes / sys.PtrSize)}

mheap 初始化之后,对当前的线程也就是 M 进行初始化。

//获取当前 G

_g_ := getg()

// 获取 G 上绑定的 M 的 mcache

_g_.m.mcache = allocmcache() 3.4 per-P mcache 初始化

上面好像并没有说到针对 P 的 mcache 初始化,因为这个时候还没有初始化 P。我们看一下 bootstrap 的代码。

funcschedinit() { ... mallocinit() ... ifprocs > _MaxGomaxprocs { procs = _MaxGomaxprocs } ifprocresize(procs) != nil{ ... }}

其中 mallocinit() 上面说过了。对 P 的初始化在函数 procresize() 中执行,我们下面只看内存相关的部分。

funcprocresize(nprocs int32) *p { ... // initialize new P'sfori := int32(0); i < nprocs; i++ { pp := allp[i] ifpp == nil{ pp = new(p) pp.id = i pp.status = _Pgcstop pp.sudogcache = pp.sudogbuf[ :0]

fori := rangepp.deferpool { pp.deferpool[i] = pp.deferpoolbuf[i][ :0] } atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp)) } // P mcache 初始化ifpp.mcache == nil{

ifold == 0&& i == 0{

ifgetg().m.mcache == nil{ throw( "missing mcache?") } // P[0] 分配给主 Goroutinepp.mcache = getg().m.mcache // bootstrap} else{ // P[0] 之外的 P 申请 mcachepp.mcache = allocmcache() } } ... } ...}

所有的 P 都存放在一个全局数组 allp 中,procresize() 的目的就是将 allp 中用到的 P 进行初始化,同时对多余 P 的资源剥离。我们看一下 allocmcache。

funcallocmcache() *mcache { lock(&mheap_.lock) c := (*mcache)(mheap_.cachealloc.alloc()) unlock(&mheap_.lock)

fori := 0; i < _NumSizeClasses; i++ { c.alloc[i] = &emptymspan } c.next_sample = nextSample() returnc}

allocmcache 是调用 mheap 中特定分配器 cachealloc 来分配的。我们前面说过 fixalloc 是一个特定大小内存块的 free list。那么 cachealloc 就是 mcache 的 free list,每次分配也很简单,直接取 list 表头就可以了。mcache.alloc 现在并没有分配,是从 mcentral 中分配的。

4. 内存分配

先说一下给对象 object 分配内存的主要流程:

object size > 32K,则使用 mheap 直接分配。

object size < 16 byte,使用 mcache 的小对象分配器 tiny 直接分配。 (其实 tiny 就是一个指针,暂且这么说吧。)

object size > 16 byte && size <=32K byte 时,先使用 mcache 中对应的 size class 分配。

如果 mcache 对应的 size class 的 span 已经没有可用的块,则向 mcentral 请求。

如果 mcentral 也没有可用的块,则向 mheap 申请,并切分。

如果 mheap 也没有合适的 span,则想操作系统申请。

我们看一下在堆上,也就是 arena 区分配内存的相关函数。

packagemain

funcfoo() * int{ x := 1return&x}

funcmain() { x := foo() println(*x)}

根据之前介绍的逃逸分析,foo() 中的 x 会被分配到堆上。把上面代码保存为 test1.go 看一下汇编代码。

$ go build -gcflags '-l'-o test1 test1.go$ go tool objdump -s"main.foo"test1TEXT main.foo(SB) /Users/didi/code/go/malloc_example/test2.go test2.go: 30x2040 65488b0c25a0080000 GS MOVQ GS: 0x8a0, CX test2.go: 30x2049 483b6110 CMPQ 0x10(CX), SP test2.go: 30x204d 762a JBE 0x2079 test2.go: 30x204f 4883ec10 SUBQ $0x10, SP test2.go: 40x2053 488d1d66460500 LEAQ 0x54666(IP), BX test2.go: 40x205a 48891c24 MOVQ BX, 0(SP) test2.go: 40x205e e82d8f0000 CALL runtime.newobject(SB) test2.go: 40x2063 488b442408 MOVQ 0x8(SP), AX test2.go: 40x2068 48c70001000000 MOVQ $0x1, 0(AX) test2.go: 50x206f 4889442418MOVQ AX, 0x18(SP) test2.go: 50x2074 4883c410 ADDQ $0x10, SP test2.go: 50x2078 c3 RET test2.go: 30x2079 e8a28d0400 CALL runtime.morestack_noctxt(SB) test2.go: 30x207e ebc0 JMP main.foo(SB)

堆上内存分配调用了 runtime 包的 newobject 函数。

funcnewobject(typ *_type) unsafe.Pointer {

returnmallocgc(typ.size, typ, true)}

funcmallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... c := gomcache()

varx unsafe.Pointer noscan := typ == nil|| typ.kind&kindNoPointers != 0ifsize <= maxSmallSize { // object size <= 32Kifnoscan && size < maxTinySize { // 小于 16 byte 的小对象分配off := c.tinyoffset

// Align tiny pointer for required (conservative) alignment.ifsize &7== 0{ off = round(off, 8) } elseifsize &3== 0{ off = round(off, 4) } elseifsize &1== 0{ off = round(off, 2) }

ifoff+size <= maxTinySize && c.tiny != 0{

// The object fits into existing tiny block.x = unsafe.Pointer(c.tiny + off) c.tinyoffset = off + size c.local_tinyallocs++ mp.mallocing = 0releasem(mp)

returnx } // Allocate a new maxTinySize block.span := c.alloc[tinySizeClass] v := nextFreeFast(span)

ifv == 0{ v, _, shouldhelpgc = c.nextFree(tinySizeClass) } x = unsafe.Pointer(v) (* [2] uint64)(x) [0] = 0(* [2] uint64)(x) [1] = 0// See if we need to replace the existing tiny block with the new one// based on amount of remaining free space.ifsize < c.tinyoffset || c.tiny == 0{ c.tiny = uintptr(x) c.tinyoffset = size } size = maxTinySize } else{ // object size >= 16 byte && object size <= 32K bytevarsizeclass uint8ifsize <= smallSizeMax -8{ sizeclass = size_to_class8[(size+smallSizeDiv -1)/smallSizeDiv] } else{ sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv -1)/largeSizeDiv] } size = uintptr(class_to_size[sizeclass]) span := c.alloc[sizeclass] v := nextFreeFast(span)

ifv == 0{ v, span, shouldhelpgc = c.nextFree(sizeclass) } x = unsafe.Pointer(v)

ifneedzero && span.needzero != 0{ memclrNoHeapPointers(unsafe.Pointer(v), size) } }} else{ //object size > 32K bytevars *mspan shouldhelpgc = truesystemstack( func() { s = largeAlloc(size, needzero) }) s.freeindex = 1s.allocCount = 1x = unsafe.Pointer(s.base()) size = s.elemsize }}

整个分配过程可以根据 object size 拆解成三部分:size < 16 byte, 16 byte <= size <= 32 K byte, size > 32 K byte。

4.1 size 小于 16 byte

对于小于 16 byte 的内存块,mcache 有个专门的内存区域 tiny 用来分配,tiny 是指针,指向开始地址。

funcmallocgc(...) { ... off := c.tinyoffset // 地址对齐ifsize &7== 0{ off = round(off, 8) } elseifsize &3== 0{ off = round(off, 4) } elseifsize &1== 0{ off = round(off, 2) } // 分配ifoff+size <= maxTinySize && c.tiny != 0{

// The object fits into existing tiny block.x = unsafe.Pointer(c.tiny + off) c.tinyoffset = off + size c.local_tinyallocs++ mp.mallocing = 0releasem(mp)

returnx } // tiny 不够了,为其重新分配一个 16 byte 内存块span := c.alloc[tinySizeClass] v := nextFreeFast(span)

ifv == 0{ v, _, shouldhelpgc = c.nextFree(tinySizeClass) } x = unsafe.Pointer(v) //将申请的内存块全置为 0(* [2] uint64)(x) [0] = 0(* [2] uint64)(x) [1] = 0// See if we need to replace the existing tiny block with the new one// based on amount of remaining free space.// 如果申请的内存块用不完,则将剩下的给 tiny,用 tinyoffset 记录分配了多少。ifsize < c.tinyoffset || c.tiny == 0{ c.tiny = uintptr(x) c.tinyoffset = size } size = maxTinySize}

如上所示,tinyoffset 表示 tiny 当前分配到什么地址了,之后的分配根据 tinyoffset 寻址。先根据要分配的对象大小进行地址对齐,比如 size 是 8 的倍数,tinyoffset 和 8 对齐。然后就是进行分配。如果 tiny 剩余的空间不够用,则重新申请一个 16 byte 的内存块,并分配给 object。如果有结余,则记录在 tiny 上。

4.2 size 大于 32 K byte

对于大于 32 Kb 的内存分配,直接跳过 mcache 和 mcentral,通过 mheap 分配。

funcmallocgc(...) { } else{

vars *mspan shouldhelpgc = truesystemstack( func() { s = largeAlloc(size, needzero) }) s.freeindex = 1s.allocCount = 1x = unsafe.Pointer(s.base()) size = s.elemsize } ...}

funclargeAlloc(size uintptr, needzero bool) *mspan { ... npages := size >> _PageShift

ifsize&_PageMask != 0{ npages++ } ... s := mheap_.alloc(npages, 0, true, needzero)

ifs == nil{ throw( "out of memory") } s.limit = s.base() + size heapBitsForSpan(s.base()).initSpan(s)

returns}

对于大于 32 K 的内存分配都是分配整数页,先右移然后低位与计算需要的页数。