前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >超全代码!详解Go中内存分配源码实现

超全代码!详解Go中内存分配源码实现

作者头像
腾讯云开发者
发布2021-06-03 21:41:36
1.1K0
发布2021-06-03 21:41:36
举报

导语 | 本文会从调试汇编入手讲解Go的内存分配实现的源码,所以在看的时候不妨自己动手调试,并且文中含有大量高质量的图片帮助理解,耐心看完一定能有所收获。注:本文使用的go源码为15.7。

一、介绍

Go 语言的内存分配器借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心理念是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。TCMalloc 相关的信息可以看这里:

http://goog-perftools.sourceforge.net/doc/tcmalloc.html

即如果要分配的对象是个小对象(<= 32k),在每个线程中都会有一个无锁的小对象缓存,可以直接高效的无锁的方式进行分配;

如下:对象被分到不同的内存大小组中的链表中。

如果是个大对象(>32k),那么页堆进行分配。如下:

虽然 go 内存分配器最初是基于 tcmalloc 的,但是现在已经有了很大的不同。所以上面的一些结构会有些许变化,下面会提及。

因为内存分配的源码比较复杂,为了方便大家调试,所以在进行源码分析之前,先看看如何断点汇编进行调试。

1. 断点调试汇编

目前 Go 语言支持 GDB、LLDB 和 Delve 几种调试器。只有 Delve 是专门为 Go 语言设计开发的调试工具。而且 Delve 本身也是采用 Go 语言开发,对 Windows 平台也提供了一样的支持。本节我们基于 Delve 简单解释如何调试 Go 汇编程序。项目地址:https://github.com/go-delve/delve

安装:

代码语言:javascript
复制
go get github.com/go-delve/delve/cmd/dlv

首先编写一个 test.go 的例子:

代码语言:javascript
复制
package main

import "fmt"

type A struct {test string}func main() {a := new(A)fmt.Println(a)}

然后命令行进入包所在目录,然后输入 dlv debug 命令进入调试:

代码语言:javascript
复制
PS C:\document\code\test_go\src> dlv debugType 'help' for list of commands.

然后可以使用 break 命令在 main 包的 main 方法上设置一个断点:

代码语言:javascript
复制
(dlv) break main.mainBreakpoint 1 set at 0x4bd30a for main.main() c:/document/code/test_go/src/test.go:8

通过 breakpoints 查看已经设置的所有断点:

代码语言:javascript
复制
(dlv) breakpointsBreakpoint runtime-fatal-throw at 0x4377e0 for runtime.fatalthrow() c:/software/go/src/runtime/panic.go:1162 (0)Breakpoint unrecovered-panic at 0x437860 for runtime.fatalpanic() c:/software/go/src/runtime/panic.go:1189 (0)        print runtime.curg._panic.argBreakpoint 1 at 0x4bd30a for main.main() c:/document/code/test_go/src/test.go:8 (0)

通过 continue 命令让程序运行到下一个断点处:

代码语言:javascript
复制
(dlv) continue> main.main() c:/document/code/test_go/src/test.go:8 (hits goroutine(1):1 total:1) (PC: 0x4bd30a)     3: import "fmt"     4:     5: type A struct {     6:         test string     7: }=>   8: func main() {     9:         a := new(A)    10:         fmt.Println(a)    11: }    12:    13:

通过 disassemble 反汇编命令查看 main 函数对应的汇编代码:

代码语言:javascript
复制
(dlv) disassembleTEXT main.main(SB) C:/document/code/test_go/src/test.go        test.go:8       0x4bd2f0        65488b0c2528000000      mov rcx, qword ptr gs:[0x28]        test.go:8       0x4bd2f9        488b8900000000          mov rcx, qword ptr [rcx]        test.go:8       0x4bd300        483b6110                cmp rsp, qword ptr [rcx+0x10]        test.go:8       0x4bd304        0f8697000000            jbe 0x4bd3a1=>      test.go:8       0x4bd30a*       4883ec78                sub rsp, 0x78        test.go:8       0x4bd30e        48896c2470              mov qword ptr [rsp+0x70], rbp        test.go:8       0x4bd313        488d6c2470              lea rbp, ptr [rsp+0x70]        test.go:9       0x4bd318        488d0581860100          lea rax, ptr [__image_base__+874912]        test.go:9       0x4bd31f        48890424                mov qword ptr [rsp], rax        test.go:9       0x4bd323        e8e800f5ff              call $runtime.newobject        test.go:9       0x4bd328        488b442408              mov rax, qword ptr [rsp+0x8]        test.go:9       0x4bd32d        4889442430              mov qword ptr [rsp+0x30], rax        test.go:10      0x4bd332        4889442440              mov qword ptr [rsp+0x40], rax        test.go:10      0x4bd337        0f57c0                  xorps xmm0, xmm0        test.go:10      0x4bd33a        0f11442448              movups xmmword ptr [rsp+0x48], xmm0        test.go:10      0x4bd33f        488d442448              lea rax, ptr [rsp+0x48]        test.go:10      0x4bd344        4889442438              mov qword ptr [rsp+0x38], rax        test.go:10      0x4bd349        8400                    test byte ptr [rax], al        test.go:10      0x4bd34b        488b4c2440              mov rcx, qword ptr [rsp+0x40]        test.go:10      0x4bd350        488d15099f0000          lea rdx, ptr [__image_base__+815712]        test.go:10      0x4bd357        4889542448              mov qword ptr [rsp+0x48], rdx        test.go:10      0x4bd35c        48894c2450              mov qword ptr [rsp+0x50], rcx        test.go:10      0x4bd361        8400                    test byte ptr [rax], al        test.go:10      0x4bd363        eb00                    jmp 0x4bd365        test.go:10      0x4bd365        4889442458              mov qword ptr [rsp+0x58], rax        test.go:10      0x4bd36a        48c744246001000000      mov qword ptr [rsp+0x60], 0x1        test.go:10      0x4bd373        48c744246801000000      mov qword ptr [rsp+0x68], 0x1        test.go:10      0x4bd37c        48890424                mov qword ptr [rsp], rax        test.go:10      0x4bd380        48c744240801000000      mov qword ptr [rsp+0x8], 0x1        test.go:10      0x4bd389        48c744241001000000      mov qword ptr [rsp+0x10], 0x1        test.go:10      0x4bd392        e869a0ffff              call $fmt.Println        test.go:11      0x4bd397        488b6c2470              mov rbp, qword ptr [rsp+0x70]        test.go:11      0x4bd39c        4883c478                add rsp, 0x78        test.go:11      0x4bd3a0        c3                      ret        test.go:8       0x4bd3a1        e82a50faff              call $runtime.morestack_noctxt        .:0             0x4bd3a6        e945ffffff              jmp $main.main

现在我们可以使用 break 断点到 runtime.newobject 函数的调用上:

代码语言:javascript
复制
(dlv) break runtime.newobjectBreakpoint 2 set at 0x40d426 for runtime.newobject() c:/software/go/src/runtime/malloc.go:1164

输入 continue 跳到断点的位置:

代码语言:javascript
复制
(dlv) continue> runtime.newobject() c:/software/go/src/runtime/malloc.go:1164 (hits goroutine(1):1 total:1) (PC: 0x40d426)Warning: debugging optimized function  1159: }  1160:  1161: // implementation of new builtin  1162: // compiler (both frontend and SSA backend) knows the signature  1163: // of this function=>1164: func newobject(typ *_type) unsafe.Pointer {  1165:         return mallocgc(typ.size, typ, true)  1166: }  1167:  1168: //go:linkname reflect_unsafe_New reflect.unsafe_New  1169: func reflect_unsafe_New(typ *_type) unsafe.Pointer {

print 命令来查看 typ 的数据:

代码语言:javascript
复制
(dlv) print typ*runtime._type {size: 16, ptrdata: 8, hash: 875453117, tflag: tflagUncommon|tflagExtraStar|tflagNamed (7), align: 8, fieldAlign: 8, kind: 25, equal: runtime.strequal, gcdata: *1, str: 5418, ptrToThis: 37472}

可以看到这里打印的 size 是 16bytes,因为我们A结构体里面就一个 string 类型的 field。

进入到 mallocgc 方法后,通过 args 和 locals 命令查看函数的参数和局部变量:

代码语言:javascript
复制
(dlv) argssize = (unreadable could not find loclist entry at 0x8b40 for address 0x40ca73)typ = (*runtime._type)(0x4d59a0)needzero = true~r3 = (unreadable empty OP stack)(dlv) locals(no locals)

2. 各个对象入口

我们根据汇编可以判断,所有的函数入口都是 runtime.mallocgc,但是下面两个对象需要注意一下:

  • int64对象 runtime.convT64
代码语言:javascript
复制
func convT64(val uint64) (x unsafe.Pointer) {if val < uint64(len(staticuint64s)) {x = unsafe.Pointer(&staticuint64s[val])} else {x = mallocgc(8, uint64Type, false)*(*uint64)(x) = val}return}

这段代码表示如果一个 int64 类型的值小于 256,直接使用的是缓存值,那么这个值不会进行内存分配。

  • string对象 runtime.convTstring
代码语言:javascript
复制
func convTstring(val string) (x unsafe.Pointer) {if val == "" {x = unsafe.Pointer(&zeroVal[0])} else {x = mallocgc(unsafe.Sizeof(val), stringType, true)*(*string)(x) = val}return}

由这段代码显示,如果是创建一个为”“的 string 对象,那么会直接返回一个固定的地址值,不会进行内存分配。

3. 调试用例

大家在调试的时候也可以使用下面的例子来进行调试,因为 go 里面的对象分配是分为大对象、小对象、微对象的,所以下面准备了三个方法分别对应三种对象的创建时的调试。

代码语言:javascript
复制
type smallobj struct {arr [1 << 10]byte}

type largeobj struct {arr [1 << 26]byte}

func tiny()   {y := 100000fmt.Println(y)}

func large() {large := largeobj{}println(&large)}

func small() {small := smallobj{}print(&small)}

func main() {//tiny()//small()//large() }

二、分析

1. 分配器的组件

内存分配是由内存分配器完成,分配器由3种组件构成:runtime.mspan、runtime.mcache、runtime.mcentral、runtime.mheap。

  • runtime.mspan
代码语言:javascript
复制
type mspan struct {// 上一个节点next *mspan     // 下一个节点prev *mspan      // span集合list *mSpanList  // span开始的地址值startAddr uintptr  // span管理的页数npages    uintptr  
// Object n starts at address n*elemsize + (start << pageShift).// 空闲节点的索引freeindex uintptr // span中存放的对象数量nelems uintptr  
// 用于快速查找内存中未被使用的内存allocCache uint64   // 用于计算mspan管理了多少内存  elemsize    uintptr  // span的结束地址值  limit       uintptr
...}

runtime.mspan 是内存管理器里面的最小粒度单元,所有的对象都被管理在 mspan 下。

mspan 是一个链表,有上下指针;

npages 代表 mspan 管理的堆页的数量;

freeindex 是空闲对象的索引;

nelems 代表这个 mspan 中可以存放多少对象,等于(npages * pageSize)/elemsize;

allocCache 用于快速的查找未被使用的内存地址;

elemsize 表示一个对象会占用多个 bytes,等于 class_to_size[sizeclass],需要注意的是 sizeclass 每次获取的时候会 sizeclass 方法,将 sizeclass>>1;

limit 表示 span 结束的地址值,等于 startAddr+ npages*pageSize;

实例图如下:

图中 alloc 是一个拥有137个元素的 mspan 数组,mspan 数组管理数个 page 大小的内存,每个 page 是 8k,page 的数量由 spanclass 规格决定。

  • runtime.mcache
代码语言:javascript
复制
type mcache struct { ...// 申请小对象的起始地址tiny             uintptr// 从起始地址tiny开始的偏移量tinyoffset       uintptr// tiny对象分配的数量local_tinyallocs uintptr // number of tiny allocs not counted in other stats// mspan对象集合,numSpanClasses=134alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass...}

runtime.mcache 是绑在并发模型 GPM 的 P 上,在分配微对象和小对象的时候会先去 runtime.mcache 中获取,每一个处理器都会被分配一个线程缓存 runtime.mcache,因此从 runtime.mcache 进行分配时无需加锁。

在 runtime.mcache 中有一个 alloc 数组,是 runtime.mspan 的集合,runtime.mspan 是?Go?语言内存管理的基本单元。对于[16B,32KB]的对象会使用这部分 span 进行内存分配,所以所有在这区间大小的对象都会从 alloc 这个数组里寻找,下面会分析到。

  • runtime.mcentral
代码语言:javascript
复制
type mcentral struct { //spanClass Idspanclass spanClass// 空闲的span列表partial [2]spanSet // list of spans with a free object// 已经被使用的span列表full    [2]spanSet // list of spans with no free objects

//分配mspan的累积计数nmalloc uint64}

当 runtime.mcache 中空间不足的时候,会去 runtime.mcentral 中申请对应规格的 mspan。获取 mspan 的时候会从 partial 列表和 full 列表中获取,获取的时候会使用无锁的方式获取。

在 runtime.mcentral 中,有 spanclass 标识,spanclass 表示这个 mcentral 的类型,下面我们会看到,在分配[16B,32KB]大小对象的时候,会将对象的大小分成67组:

代码语言:javascript
复制
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

所以 runtime.mcentral 只负责一种 spanclass 规格类型。

runtime.mcentral 的数据会由两个 spanSet 托管,partial 负责空闲的列表,full 负责已被使用的列表。

代码语言:javascript
复制
type headTailIndex uint64

type spanSet struct { // lockspineLock mutex// 数据块的指针spine     unsafe.Pointer // *[N]*spanSetBlock, accessed atomically// lenspineLen  uintptr        // Spine array length, accessed atomically// capspineCap  uintptr        // Spine array cap, accessed under lock

// 头尾的指针,前32位是头指针,后32位是尾指针index headTailIndex}

spanSet 这个数据结构里面有一个由 index 组成的头尾指针,pop 数据的时候会从头获取,push 数据的时候从 tail 放入,spine 相当于数据块的指针,通过 head 和 tail 的位置可以算出每个数据块的具体位置,数据块由 spanSetBlock 表示:

代码语言:javascript
复制
const spanSetBlockEntries = 512type spanSetBlock struct {...spans [spanSetBlockEntries]*mspan}

spanSetBlock 是一个存放 mspan 的数据块,里面会包含一个存放512个 mspan 的数据指针。所以 mcentral 的总体数据结构如下:

  • runtime.mheap
代码语言:javascript
复制
type mheap struct { lock      mutexpages     pageAlloc // page allocation data structure 
//arenas数组集合,一个二维数组arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena

//各个规格的mcentral集合central [numSpanClasses]struct {mcentral mcentralpad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte}...}

对于 runtime.mheap 需要关注 central 和 arenas。central 是各个规格的 mcentral 集合,在初始化的时候会通过遍历 class_to_size 来进行创建;arenas 是一个二维数组,用来管理内存空间。arenas 由多个 runtime.heapArena 组成,每个单元都会管理 64MB 的内存空间:

代码语言:javascript
复制
const (pageSize             = 8192                       // 8KBheapArenaBytes       = 67108864                   // 64MB pagesPerArena        = heapArenaBytes / pageSize  // 8192)

type heapArena struct {bitmap [heapArenaBitmapBytes]bytespans [pagesPerArena]*mspanpageInUse [pagesPerArena / 8]uint8pageMarks [pagesPerArena / 8]uint8zeroedBase uintptr}

需要注意的是,上面的 heapArenaBytes 代表的64M只是在除 windows 以外的64位机器才会显示,在 windows 机器上显示的是4MB。具体可以看下面的官方注释:

代码语言:javascript
复制
//       Platform  Addr bits  Arena size  L1 entries   L2 entries// --------------  ---------  ----------  ----------  -----------//       */64-bit         48        64MB           1    4M (32MB)// windows/64-bit         48         4MB          64    1M  (8MB)//       */32-bit         32         4MB           1  1024  (4KB)//     */mips(le)         31         4MB           1   512  (2KB)

L1 entries、L2 entries分别代表的是 runtime.mheap 中 arenas 一维、二维的值。

2. 给对象分配内存

我们通过对源码的反编译可以知道,堆上所有的对象都会通过调用 runtime.newobject 函数分配内存,该函数会调用 runtime.mallocgc:

代码语言:javascript
复制
//创建一个新的对象func newobject(typ *_type) unsafe.Pointer {    //size表示该对象的大小return mallocgc(typ.size, typ, true)}

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ...  dataSize := size// 获取mcache,用于处理微对象和小对象的分配c := gomcache()var x unsafe.Pointer// 表示对象是否包含指针,true表示对象里没有指针noscan := typ == nil || typ.ptrdata == 0// maxSmallSize=32768 32kif size <= maxSmallSize {// maxTinySize= 16 bytes if noscan && size < maxTinySize {...} else {...}// 大于 32 Kb 的内存分配,通过 mheap 分配} else {...} ... return x}

通过 mallocgc 的代码可以知道,mallocgc 在分配内存的时候,会按照对象的大小分为3档来进行分配:

1)小于16bytes的小对象;

2)在16bytes与32K之间的微对象;

3)大于32Kb的大对象;

3. 大对象分配

代码语言:javascript
复制
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ...  var s *mspanshouldhelpgc = truesystemstack(func() {s = largeAlloc(size, needzero, noscan)})s.freeindex = 1s.allocCount = 1x = unsafe.Pointer(s.base())size = s.elemsize... return x}

从上面我们可以看到分配大于32KB的空间时,直接使用 largeAlloc 来分配一个 mspan。

代码语言:javascript
复制
func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan {// _PageSize=8k,也就是表明对象太大,溢出if size+_PageSize < size {throw("out of memory")}// _PageShift==13,计算需要分配的页数npages := size >> _PageShift// 如果不是整数,多出来一些,需要加1if size&_PageMask != 0 {npages++} ...// 从堆上分配s := mheap_.alloc(npages, makeSpanClass(0, noscan), needzero)if s == nil {throw("out of memory")}...return s}

在分配内存的时候是按页来进行分配的,每个页的大小是_PageSize(8K),然后需要根据传入的size来判断需要分多少页,最后调用 alloc 从堆上分配。

代码语言:javascript
复制
func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan {var s *mspansystemstack(func() { if h.sweepdone == 0 {// 回收一部分内存h.reclaim(npages)}// 进行内存分配s = h.allocSpan(npages, false, spanclass, &memstats.heap_inuse)}) ...return s}

继续看 allocSpan 的实现:

代码语言:javascript
复制
const pageCachePages = 8 * unsafe.Sizeof(pageCache{}.cache)

func (h *mheap) allocSpan(npages uintptr, manual bool, spanclass spanClass, sysStat *uint64) (s *mspan) {// Function-global state.gp := getg()base, scav := uintptr(0), uintptr(0)
pp := gp.m.p.ptr()// 申请的内存比较小,尝试从pcache申请内存if pp != nil && npages < pageCachePages/4 {c := &pp.pcache
if c.empty() {lock(&h.lock)*c = h.pages.allocToCache()unlock(&h.lock)} 

base, scav = c.alloc(npages)if base != 0 {s = h.tryAllocMSpan()

if s != nil && gcBlackenEnabled == 0 && (manual || spanclass.sizeclass() != 0) {goto HaveSpan} }} lock(&h.lock)// 内存比较大或者线程的页缓存中内存不足,从mheap的pages上获取内存if base == 0 { base, scav = h.pages.alloc(npages)// 内存也不够,那么进行扩容if base == 0 {if !h.grow(npages) {unlock(&h.lock)return nil}// 重新申请内存base, scav = h.pages.alloc(npages)// 内存不足,抛出异常if base == 0 {throw("grew heap, but no adequate free space found")}}}if s == nil { // 分配一个mspan对象s = h.allocMSpanLocked()}
unlock(&h.lock)

HaveSpan: // 设置参数初始化s.init(base, npages) ...// 建立mheap与mspan之间的联系h.setSpans(s.base(), npages, s)...return s}

这里会根据需要分配的内存大小再判断一次:

  • 如果要分配的页数小于pageCachePages/4=64/4=16页,那么就尝试从pcache申请内存;
  • 如果申请的内存比较大或者线程的页缓存中内存不足,会通过runtime.pageAlloc.alloc从页堆分配内存;
  • 如果页堆上内存不足,那么就mheap的grow方法从系统上申请内存,然后再调用pageAlloc的alloc分配内存;

下面来看看 grow 的向操作系统申请内存:

代码语言:javascript
复制
func (h *mheap) grow(npage uintptr) bool {// We must grow the heap in whole palloc chunks.ask := alignUp(npage, pallocChunkPages) * pageSize

totalGrowth := uintptr(0)nBase := alignUp(h.curArena.base+ask, physPageSize)// 内存不够则调用sysAlloc申请内存if nBase > h.curArena.end { 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}// 重新设置curArena的值if uintptr(av) == h.curArena.end { h.curArena.end = uintptr(av) + asize} else { if size := h.curArena.end - h.curArena.base; size != 0 {h.pages.grow(h.curArena.base, size)totalGrowth += size} h.curArena.base = uintptr(av)h.curArena.end = uintptr(av) + asize} nBase = alignUp(h.curArena.base+ask, physPageSize)} ...return true}

grow 会通过 curArena的end 值来判断是不是需要从系统申请内存;如果 end小于nBase 那么会调用 runtime.mheap.sysAlloc 方法从操作系统中申请更多的内存;

代码语言:javascript
复制
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {n = alignUp(n, heapArenaBytes)
// 在预先保留的内存中申请一块可以使用的空间v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys)if v != nil {size = ngoto mapped} // 根据页堆的arenaHints在目标地址上尝试扩容for h.arenaHints != nil {hint := h.arenaHintsp := hint.addrif hint.down {p -= n}if p+n < p {// We can't use this, so don't ask.v = nil} else if arenaIndex(p+n-1) >= 1<<arenaBits {// Outside addressable heap. Can't use.v = nil} else {// 从操作系统中申请内存v = sysReserve(unsafe.Pointer(p), n)}if p == uintptr(v) {// Success. Update the hint.if !hint.down {p += n}hint.addr = psize = nbreak} if v != nil {sysFree(v, n, nil)}h.arenaHints = hint.nexth.arenaHintAlloc.free(unsafe.Pointer(hint))}  ...// 将内存由Reserved转为PreparedsysMap(v, size, &memstats.heap_sys)

mapped:// Create arena metadata.// 初始化一个heapArena来管理刚刚申请的内存for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {l2 := h.arenas[ri.l1()]if l2 == nil { l2 = (*[1 << arenaL2Bits]*heapArena)(persistentalloc(unsafe.Sizeof(*l2), sys.PtrSize, nil))if l2 == nil {throw("out of memory allocating heap arena map")}atomic.StorepNoWB(unsafe.Pointer(&h.arenas[ri.l1()]), unsafe.Pointer(l2))}  var r *heapArenar = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))...  // 将创建heapArena放入到arenas列表中h.allArenas = h.allArenas[:len(h.allArenas)+1]h.allArenas[len(h.allArenas)-1] = riatomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r))}return}

sysAlloc方法会调用 runtime.linearAlloc.alloc 预先保留的内存中申请一块可以使用的空间;如果没有会调用sysReserve方法会从操作系统中申请内存;最后初始化一个heapArena来管理刚刚申请的内存,然后将创建heapArena放入到arenas列表中。

至此,大对象的分配流程至此结束。

4. 小对象分配

对于介于16bytes~32K的对象分配如下:

代码语言:javascript
复制
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...dataSize := size// 获取mcache,用于处理微对象和小对象的分配c := gomcache()var x unsafe.Pointer// 表示对象是否包含指针,true表示对象里没有指针noscan := typ == nil || typ.ptrdata == 0// maxSmallSize=32768 32kif size <= maxSmallSize {// maxTinySize= 16 bytes if noscan && size < maxTinySize { ...} else {var sizeclass uint8//计算 sizeclass// smallSizeMax=1024if size <= smallSizeMax-8 {// smallSizeDiv=8sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]} else {// largeSizeDiv=128,smallSizeMax = 1024sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]}size = uintptr(class_to_size[sizeclass])spc := makeSpanClass(sizeclass, noscan)span := c.alloc[spc]//从对应的 span 里面分配一个 object v := nextFreeFast(span)if v == 0 {// mcache不够用了,则从 mcentral 申请内存到 mcachev, span, shouldhelpgc = c.nextFree(spc)}x = unsafe.Pointer(v)if needzero && span.needzero != 0 {memclrNoHeapPointers(unsafe.Pointer(v), size)}} ...}  ...return x}

首先会先计算 sizeclass 大小,计算 sizeclass 是通过预先定义两个数组:size_to_class8 和 size_to_class128。小于 1024 - 8 = 1016 (smallSizeMax=1024),使用 size_to_class8,否则使用数组 size_to_class128。

举个例子,比如要分配 20 byte 的内存,那么sizeclass = size_to_calss8[(20+7)/8] = size_to_class8[3] = 3。然后通过class_to_size[3] 获取到对应的值32,表示应该要分配32bytes的内存值。

接着会从 alloc 数组中获取一个 span 的指针,通过调用 nextFreeFast 尝试从 mcache 中获取内存,如果 mcache 不够用了,则尝试调用 nextFree从mcentral 申请内存到 mcache。

下面看看nextFreeFast:

代码语言:javascript
复制
func nextFreeFast(s *mspan) gclinkptr {    // 获取allocCache二进制中0的个数theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache?if theBit < 64 {result := s.freeindex + uintptr(theBit)if result < s.nelems {freeidx := result + 1if freeidx%64 == 0 && freeidx != s.nelems {return 0}s.allocCache >>= uint(theBit + 1)s.freeindex = freeidxs.allocCount++return gclinkptr(result*s.elemsize + s.base())}}return 0}

allocCache在初始化的时候会初始化成^uint64(0),换算成二进制,如果为0则表示被占用,通过allocCache可以快速的定位待分配的空间:

代码语言:javascript
复制
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {s = c.alloc[spc]shouldhelpgc = false// 当前span中找到合适的index索引freeIndex := s.nextFreeIndex()// 当前span已经满了if freeIndex == s.nelems { if uintptr(s.allocCount) != s.nelems {println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems)throw("s.allocCount != s.nelems && freeIndex == s.nelems")}// 从 mcentral 中获取可用的span,并替换掉当前 mcache里面的spanc.refill(spc)shouldhelpgc = trues = c.alloc[spc]// 再次到新的span里面查找合适的indexfreeIndex = s.nextFreeIndex()}

if freeIndex >= s.nelems {throw("freeIndex is not valid")}// 计算出来内存地址,并更新span的属性v = gclinkptr(freeIndex*s.elemsize + s.base())s.allocCount++if uintptr(s.allocCount) > s.nelems {println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems)throw("s.allocCount > s.nelems")}return}

nextFree中会判断当前span是不是已经满了,如果满了就调用refill方法从mcentral中获取可用的span,并替换掉当前mcache里面的span。

代码语言:javascript
复制
func (c *mcache) refill(spc spanClass) { s := c.alloc[spc]...s = mheap_.central[spc].mcentral.cacheSpan()if s == nil {throw("out of memory")} ...c.alloc[spc] = s}

Refill根据指定的sizeclass获取对应的span,并作为mcache的新的sizeclass对应的span。

代码语言:javascript
复制
func (c *mcentral) cacheSpan() *mspan {...sg := mheap_.sweepgenspanBudget := 100

var s *mspan
// 从清理过的、包含空闲空间的spanSet结构中查找可以使用的内存管理单元if s = c.partialSwept(sg).pop(); s != nil {goto havespan} for ; spanBudget >= 0; spanBudget-- {// 从未被清理过的、有空闲对象的spanSet查找可用的spans = c.partialUnswept(sg).pop()if s == nil {break}if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {// 找到要回收的span,触发sweep进行清理s.sweep(true)goto havespan}}for ; spanBudget >= 0; spanBudget-- {// 获取未被清理的、不包含空闲空间的spanSet查找可用的spans = c.fullUnswept(sg).pop()if s == nil {break}if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {s.sweep(true)freeIndex := s.nextFreeIndex()if freeIndex != s.nelems {s.freeindex = freeIndexgoto havespan}c.fullSwept(sg).push(s)}}// 从堆中申请新的内存管理单元s = c.grow()if s == nil {return nil} havespan:n := int(s.nelems) - int(s.allocCount)if n == 0 || s.freeindex == s.nelems || uintptr(s.allocCount) == s.nelems {throw("span has no free objects")} //更新 nmallocatomic.Xadd64(&c.nmalloc, int64(n))usedBytes := uintptr(s.allocCount) * s.elemsizeatomic.Xadd64(&memstats.heap_live, int64(spanBytes)-int64(usedBytes))if trace.enabled {// heap_live changed.traceHeapAlloc()}if gcBlackenEnabled != 0 {// heap_live changed.gcController.revise()}freeByteBase := s.freeindex &^ (64 - 1)whichByte := freeByteBase / 8 // 更新allocCaches.refillAllocCache(whichByte)  // s.allocCache.s.allocCache >>= s.freeindex % 64 return s}

cacheSpan主要是从mcentral的spanset中去寻找可用的span,如果没找到那么调用grow方法从堆中申请新的内存管理单元。

获取到后更新nmalloc、allocCache等字段。

runtime.mcentral.grow触发扩容操作从堆中申请新的内存:

代码语言:javascript
复制
func (c *mcentral) grow() *mspan {// 获取待分配的页数npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])size := uintptr(class_to_size[c.spanclass.sizeclass()])// 获取新的spans := mheap_.alloc(npages, c.spanclass, true)if s == nil {return nil}

// Use division by multiplication and shifts to quickly compute:// n := (npages << _PageShift) / sizen := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2// 初始化limit s.limit = s.base() + size*nheapBitsForAddr(s.base()).initSpan(s)return s}

grow里面会调用runtime.mheap.alloc方法获取span,这个方法在上面已经讲过了,不记得的同学可以翻一下。

到这里小对象的分配就讲解完毕了。

5. 微对象分配

代码语言:javascript
复制
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...dataSize := size// 获取mcache,用于处理微对象和小对象的分配c := gomcache()var x unsafe.Pointer// 表示对象是否包含指针,true表示对象里没有指针noscan := typ == nil || typ.ptrdata == 0// maxSmallSize=32768 32kif size <= maxSmallSize {// maxTinySize= 16 bytes if noscan && size < maxTinySize { off := c.tinyoffset // 指针内存对齐if size&7 == 0 {off = alignUp(off, 8)} else if size&3 == 0 {off = alignUp(off, 4)} else if size&1 == 0 {off = alignUp(off, 2)}// 判断指针大小相加是否超过16if off+size <= maxTinySize && c.tiny != 0 {// 获取tiny空闲内存的起始位置x = unsafe.Pointer(c.tiny + off)// 重设偏移量c.tinyoffset = off + size// 统计数量c.local_tinyallocs++mp.mallocing = 0releasem(mp)return x}  // 重新分配一个内存块span := c.alloc[tinySpanClass]v := nextFreeFast(span)if v == 0 {v, _, shouldhelpgc = c.nextFree(tinySpanClass)}x = unsafe.Pointer(v)//将申请的内存块全置为 0(*[2]uint64)(x)[0] = 0(*[2]uint64)(x)[1] = 0 // 如果申请的内存块用不完,则将剩下的给 tiny,用 tinyoffset 记录分配了多少。if size < c.tinyoffset || c.tiny == 0 {c.tiny = uintptr(x)c.tinyoffset = size}size = maxTinySize}  ...}  ...return x}

在分配对象内存的时候做了一个判断, 如果该对象的大小小于16bytes,并且是不包含指针的,那么就可以看作是微对象。

在分配微对象的时候,会先判断一下tiny指向的内存块够不够用,如果tiny剩余的空间超过了size大小,那么就直接在tiny上分配内存返回;

这里我再次使用我上面的图来加以解释。首先会去mcache数组里面找到对应的span,tinySpanClass对应的span的属性如下:

代码语言:javascript
复制
startAddr: 824635752448,npages: 1,manualFreeList: 0,freeindex: 128,nelems: 512,elemsize: 16,limit: 824635760640,allocCount: 128,spanclass: tinySpanClass (5),...

tinySpanClass对应的mspan里面只有一个page,里面的元素可以装512(nelems)个;page里面每个对象的大小是16bytes(elemsize),目前已分配了128个对象(allocCount),当然我上面的page画不了这么多,象征性画了一下。

上面的图中还画了在page里面其中的一个object已经被使用了12bytes,还剩下4bytes没有被使用,所以会更新tinyoffset与tiny的值。

三、总结

本文先介绍了如何对go的汇编进行调试,然后分了三个层次来讲解go中的内存分配是如何进行的。对于小于32k的对象来说,go通过无锁的方式可以直接从mcache获取到对应的内存,如果mcache内存不够,先是会到mcentral中获取内存,最后才到mheap中申请内存。对于大对象(>32k)来说可以直接mheap中申请,但是也有一定的优化,当大对象需要分配的页小于16页时会直接从pageCache中分配,否则才从堆页中获取。

参考资料:

https://chai2010.cn/advanced-go-programming-book/ch3-asm/ch3-09-debug.html

https://deepu.tech/memory-management-in-golang/

https://medium.com/@ankur_anand/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed

http://goog-perftools.sourceforge.net/doc/tcmalloc.html

https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator

作者简介

罗志贇,腾讯后端开发工程师,负责IEG游戏推荐相关后端业务,深入研究过Go runtime相关代码,喜欢钻研技术细节,探索技术中有趣的实现分享给大家。


6月5日,Techo TVP 开发者峰会 ServerlessDays China 2021,即将重磅来袭!

扫码立即参会赢好礼?

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-03,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 分配器的组件
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com