Go语言内置运行时(就是runtime),不同于传统的内存分配方式,go为自主管理,最开始是基于tcmalloc架构,后面逐步迭新。自主管理可实现更好的内存使用模式,如内存池、预分配等,从而避免了系统调用所带来的性能问题。
1. 基本策略
- 每次从操作系统申请一大块内存,然后将其按特定大小分成小块,构成链表(组织方式是一个单链表数组,数组的每个元素是一个单链表,链表中的每个元素具有相同的大小。);
- 为对象分配内存时从大小合适的链表提取一小块,避免每次都向操作系统申请内存,减少系统调用。
- 回收对象内存时将该小块重新归还到原链表,以便复用;若闲置内存过多,则归还部分内存到操作系统,降低整体开销。
1.1 内存块
span:即上面所说的操作系统分配的大块内存,由多个地址连续的页组成;
object:由span按特定大小切分的小块内存,每一个可存储一个对象;
按照用途,span面向内部管理,object面向对象分配。
关于span
内存分配器按照页数来区分不同大小的span,如以页数为单位将span存放到管理数组中,且以页数作为索引;
span大小并非不变,在没有获取到合适大小的闲置span时,返回页数更多的span,然后进行剪裁,多余的页数构成新的span,放回管理数组;
分配器还可以将相邻的空闲span合并,以构建更大的内存块,减少碎片提供更灵活的分配策略。
分配的内存块大小
在$GOROOT/src/runtime/malloc.go文件下可以找到相关信息。
1 //malloc.go 2 _PageShift = 13 3 _PageSize = 1<< _PageShift //8KB
用于存储对象的object,按8字节倍数分为n种。如,大小为24的object可存储范围在17~24字节的对象。在造成一些内存浪费的同时减少了小块内存的规格,优化了分配和复用的管理策略。
分配器还会将多个微小对象组合到一个object块内,以节约内存。
1 //malloc.go 2 _NumSizeClasses = 67
1 //mheap.go 2 type mspan struct { 3 next *mspan //双向链表 next span in list, or nil if none 4 prev *mspan //previous span in list, or nil if none 5 list *mSpanList //用于调试。TODO: Remove. 6 7 //起始序号 = (address >> _PageShift) 8 startAddr uintptr //address of first byte of span aka s.base() 9 npages uintptr //number of pages in span 10 11 //待分配的object链表 12 manualFreeList gclinkptr //list of free objects in mSpanManual spans 13 }
分配器初始化时,会构建对照表存储大小和规格的对应关系,包括用来切分的span页数。
1 //msize.go 2 3 // Malloc small size classes. 4 // 5 // See malloc.go for overview. 6 // See also mksizeclasses.go for how we decide what size classes to use. 7 8 package runtime 9 10 // 如果需要,返回mallocgc将分配的内存块的大小。 11 func roundupsize(size uintptr) uintptr { 12 if size < _MaxSmallSize { 13 if size <= smallSizeMax-8 { 14 return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]]) 15 } else { 16 return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]]) 17 } 18 } 19 if size+_PageSize < size { 20 return size 21 } 22 return round(size, _PageSize) 23 }
如果对象大小超出特定阈值限制,会被当做大对象(large object)特别对待。
1 //malloc.go 2 _MaxSmallSize = 32 << 10 //32KB
这里的对象分类:
- 小对象(tiny): size < 16byte;
- 普通对象: 16byte ~ 32K;
- 大对象(large):size > 32K;
1.2 内存分配器
分配器分为三个模块
cache:每个运行期工作线程都会绑定一个cache,用于无锁object分配(Central组件其实也是一个缓存,但它缓存的不是小对象内存块,而是一组一组的内存page(一个page占4k大小))。
1 //mcache.go 2 type mcache struct{ 3 以spanClass为索引管理多个用于分配的span 4 alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass 5 }
central:为所有cache提供切分好的后备span资源。
1 //mcentral.go 2 type mcentral struct{ 3 spanclass spanClass //规格 4 //链表:尚有空闲object的span 5 nonempty mSpanList // list of spans with a free object, ie a nonempty free list 6 // 链表:没有空闲object,或已被cache取走的span 7 empty mSpanList // list of spans with no free objects (or cached in an mcache) 8 } 9
heap:管理闲置span,需要时间向操作系统申请新内存(堆分配器,以8192byte页进行管理)。
1 type mheap struct{ 2 largealloc uint64 // bytes allocated for large objects 3 //页数大于127(>=127)的闲置span链表 4 largefree uint64 // bytes freed for large objects (>maxsmallsize) 5 nlargefree uint64 // number of frees for large objects (>maxsmallsize) 6 //页数在127以内的闲置span链表数组 7 nsmallfree [_NumSizeClasses]uint64 // number of frees for small objects (<=maxsmallsize) 8 //每个central对应一种sizeclass 9 central [numSpanClasses]struct { 10 mcentral mcentral 11 pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte 12 }
一个线程有一个cache对应,这个cache用来存放小对象。所有线程共享Central和Heap。
虚拟地址空间
内存分配和垃圾回收都依赖连续地址,所以系统预留虚拟地址空间,用于内存分配,申请内存时,系统承诺但不立即分配物理内存。虚拟地址分成三个区域:
- 页所属span指针数组 spans 512MB spans_mapped
- GC标记位图 bitmap 32GB bit_map
- 用户内存分配区域 arena 512GB arena_start arena_used arena_end
三个数组组成一个高性能内存管理结构。使用arena地址向操作系统申请内存,其大小决定了可分配用户内存上限;bitmap为每个对象提供4bit 标记位,用以保存指针、GC标记等信息;创建span时,按页填充对应spans空间。这些区域的相关属性保存在heap里,其中包括递进的分配位置mapped/used。
各个模块关系图如下:
1.3 内存分配流程
从对象的角度:
1、计算待分配对象规格大小(size class);
2、cache.alloc数组中找到对应规格的apan;
3、span.freelist提取可用object,若该span.freelist为空从central获取新sapn;
4、若central.nonempty为空,从heap.free/freelarge获取,并切分成object 链表;
5、如heap没有大小合适的闲置span,向操作系统申请新内存块。
释放流程:
1、将标记为可回收的object交还给所属span.freelist;
2、该span被放回central,可供任意cache重新获取使用;
3、如span已回收全部object,则将其交还给heap,以便重新切分复用;
4、定期扫描heap里长期闲置的span,释放其占用内存。
(注:以上不包括大对象,它直接从heap分配和回收)
cache为每个工作线程私有且不被共享,是实现高性能无锁分配内存的核心。central是在多个cache中提高object的利用率,避免浪费。回收操作将span交还给central后,该span可被其他cache重新获取使用。将span归还给heap是为了在不同规格object间平衡。
2. 内存分配器初始化
初始化流程:
1 func mallocinit() { 2 testdefersizes() 3 4 if heapArenaBitmapBytes&(heapArenaBitmapBytes-1) != 0 { 5 // heapBits需要位图上的模块化算法工作地址。 6 throw("heapArenaBitmapBytes not a power of 2") 7 } 8 9 // //复制类大小以用于统计信息表。 10 for i := range class_to_size { 11 memstats.by_size[i].size = uint32(class_to_size[i]) 12 } 13 14 // 检查 physPageSize. 15 if physPageSize == 0 { 16 // 操作系统初始化代码无法获取物理页面大小。 17 throw("failed to get system page size") 18 } 19 if physPageSize < minPhysPageSize { 20 print("system page size (", physPageSize, ") is smaller than minimum page size (", minPhysPageSize, ")\n") 21 throw("bad system page size") 22 } 23 if physPageSize&(physPageSize-1) != 0 { 24 print("system page size (", physPageSize, ") must be a power of 2\n") 25 throw("bad system page size") 26 } 27 28 // 初始化堆。 29 mheap_.init() 30 //为当前对象绑定cache对象 31 _g_ := getg() 32 _g_.m.mcache = allocmcache() 33 34 //创建初始 arena 增长提示。 35 if sys.PtrSize == 8 && GOARCH != "wasm" { 36 //在64位计算机上: 37 // 1.从地址空间的中间开始,可以轻松扩展到连续范围,而无需运行其他映射。 38 // 39 // 2.这使Go堆地址调试时更容易识别。 40 // 41 // 3. gccgo中的堆栈扫描仍然很保守,因此将地址与其他数据区分开很重要。 42 // 43 //在AIX上,对于64位,mmaps从0x0A00000000000000开始设置保留地址,如果失败,则尝试0x1c00000000000000~0x7fc0000000000000。 44 // 流程. 45 for i := 0x7f; i >= 0; i-- { 46 var p uintptr 47 switch { 48 case GOARCH == "arm64" && GOOS == "darwin": 49 p = uintptr(i)<<40 | uintptrMask&(0x0013<<28) 50 case GOARCH == "arm64": 51 p = uintptr(i)<<40 | uintptrMask&(0x0040<<32) 52 case GOOS == "aix": 53 if i == 0 { 54 //我们不会直接在0x0A00000000000000之后使用地址,以避免与非执行程序所完成的其他mmap发生冲突。 55 continue 56 } 57 p = uintptr(i)<<40 | uintptrMask&(0xa0<<52) 58 case raceenabled: 59 // TSAN运行时要求堆的范围为[0x00c000000000,0x00e000000000)。 60 p = uintptr(i)<<32 | uintptrMask&(0x00c0<<32) 61 if p >= uintptrMask&0x00e000000000 { 62 continue 63 } 64 default: 65 p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32) 66 } 67 hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc()) 68 hint.addr = p 69 hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 70 } 71 } else { 72 //在32位计算机上,需要更加关注保持可用堆是连续的。 73 // 74 // 1.我们为所有的heapArenas保留空间,这样它们就不会与heap交错。它们约为258MB。 75 // 76 // 2. 我们建议堆从二进制文件的末尾开始,因此我们有最大的机会保持其连续性。 77 // 78 // 3. 我们尝试放出一个相当大的初始堆保留。 79 80 const arenaMetaSize = (1 << arenaBits) * unsafe.Sizeof(heapArena{}) 81 meta := uintptr(sysReserve(nil, arenaMetaSize)) 82 if meta != 0 { 83 mheap_.heapArenaAlloc.init(meta, arenaMetaSize) 84 } 85 86 procBrk := sbrk0() 87 88 p := firstmoduledata.end 89 if p < procBrk { 90 p = procBrk 91 } 92 if mheap_.heapArenaAlloc.next <= p && p < mheap_.heapArenaAlloc.end { 93 p = mheap_.heapArenaAlloc.end 94 } 95 p = round(p+(256<<10), heapArenaBytes) 96 // // 因为我们担心32位上的碎片,所以我们尝试进行较大的初始保留。 97 arenaSizes := []uintptr{ 98 512 << 20, 99 256 << 20, 100 128 << 20, 101 } 102 for _, arenaSize := range arenaSizes { 103 a, size := sysReserveAligned(unsafe.Pointer(p), arenaSize, heapArenaBytes) 104 if a != nil { 105 mheap_.arena.init(uintptr(a), size) 106 p = uintptr(a) + size // For hint below 107 break 108 } 109 } 110 hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc()) 111 hint.addr = p 112 hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 113 } 114 }
大概流程:
1、创建对象规格大小对照表;
2、计算相关区域大小,并尝试从某个指定位置开始保留地址空间;
3、在heap里保存区域信息,包括起始位置和大小;
4、初始化heap其他属性。
看一下保留地址操作细节:
1 //mem_linux.go 2 func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer { 3 p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0) //PORT_NONE: 页面无法访问; 4 if err != 0 { 5 return nil 6 } 7 return p 8 } 9 10 func sysMap(v unsafe.Pointer, n uintptr, sysStat *uint64) { 11 mSysStatInc(sysStat, n) 12 13 p, err := mmap(v, n, _PROT_READ|_PROT_WRITE, _MAP_ANON|_MAP_FIXED|_MAP_PRIVATE, -1, 0) //_MAP_FIXED: 必须使用指定起始位置 14 if err == _ENOMEM { 15 throw("runtime: out of memory") 16 } 17 if p != v || err != 0 { 18 throw("runtime: cannot map pages in arena address space") 19 } 20 }
函数mmap()要求操作系统内核创建新的虚拟存储器区域,可指定起始位置和长度。
3. 内存分配
编译器有责任尽可能使用寄存器和栈来存储对象,有助于提升性能,减少垃圾回收器的压力。
以new函数为例看一下内存分配
1 //test.go 2 package main 3 4 import () 5 6 func test() *int { 7 x :=new(int) 8 *x = 0xAABB 9 return x 10 } 11 12 func main(){ 13 println(*test()) 14 }
在默认有内联优化的时候:
内联优化是避免栈和抢占检查这些成本的经典优化方法。
在没有内联优化的时候new函数会调用newobject在堆上分配内存。要在两个栈帧间传递对象,因此会在堆上分配而不是返回一个失效栈帧里的数据。而当内联后它实际上就成了main栈帧内的局部变量,无须去堆上操作。
GO语言支持逃逸分析(eseape, analysis), 它会在编译期通过构建调用图来分析局部变量是否会被外部调用,从而决定是否可以直接分配在栈上。
编译参数-gcflags "-m" 可输出编译优化信息,其中包括内联和逃逸分析。性能测试时使用go-test-benchemem参数可以输出堆分配次数统计。
3.1 newobject分配内存的过程
1 //mcache.go 2 3 //小对象的线程(按Go,按P)缓存。 不需要锁定,因为它是每个线程的(每个P)。 mcache是从非GC的内存中分配的,因此任何堆指针都必须进行特殊处理。 4 //go:not in heap 5 type mcache struct { 6 ... 7 // Allocator cache for tiny objects w/o pointers.See "Tiny allocator" comm ent in malloc.go. 8 // tiny指向当前微小块的开头,如果没有当前微小块,则为nil。 9 // 10 // tiny是一个堆指针。 由于mcache位于非GC的内存中,因此我们通过在标记终止期间在releaseAll中将其清除来对其进行处理。 11 tiny uintptr 12 tinyoffset uintptr 13 local_tinyallocs uintptr // 未计入其他统计信息的微小分配数 14 15 // 其余的不是在每个malloc上访问的。 16 alloc [numSpanClasses]*mspan // 要分配的范围,由spanClass索引 17 }
内置new函数的实现
1 //malloc.go 2 // implementation of new builtin 3 // compiler (both frontend and SSA backend) knows the signature 4 // of this function 5 func newobject(typ *_type) unsafe.Pointer { 6 return mallocgc(typ.size, typ, true) 7 } 8 9 // Allocate an object of size bytes. 10 // Small objects are allocated from the per-P cache's free lists. 11 // Large objects (> 32 kB) are allocated straight from the heap. 12 ///分配一个大小为字节的对象。小对象是从per-P缓存的空闲列表中分配的。 大对象(> 32 kB)直接从堆中分配。 13 func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { 14 if gcphase == _GCmarktermination { //垃圾回收有关 15 throw("mallocgc called with gcphase == _GCmarktermination") 16 } 17 18 if size == 0 { 19 return unsafe.Pointer(&zerobase) 20 } 21 if debug.sbrk != 0 { 22 align := uintptr(16) 23 if typ != nil { 24 align = uintptr(typ.align) 25 } 26 return persistentalloc(size, align, &memstats.other_sys) //围绕sysAlloc的包装程序,可以分配小块。没有相关的自由操作。用于功能/类型/调试相关的持久数据。如果align为0,则使用默认的align(当前为8)。返回的内存将被清零。考虑将持久分配的类型标记为go:notinheap。 27 } 28 29 // assistG是要为此分配收费的G,如果GC当前未激活,则为n。 30 var assistG *g 31 32 ... 33 34 // Set mp.mallocing to keep from being preempted by GC. 35 //加锁放防止GC被抢占。 36 mp := acquirem() 37 if mp.mallocing != 0 { 38 throw("malloc deadlock") 39 } 40 if mp.gsignal == getg() { 41 throw("malloc during signal") 42 } 43 mp.mallocing = 1 44 45 shouldhelpgc := false 46 dataSize := size 47 48 //当前线程所绑定的cache 49 c := gomcache() 50 var x unsafe.Pointer 51 // 判断分配的对象是否 是nil或非指针类型 52 noscan := typ == nil || typ.kind&kindNoPointers != 0 53 //微小对象 54 if size <= maxSmallSize { 55 //无须扫描非指针微小对象(16) 56 if noscan && size < maxTinySize { 57 // Tiny allocator. 58 //微小的分配器将几个微小的分配请求组合到一个内存块中。当所有子对象均不可访问时,将释放结果存储块。子对象必须是noscan(没有指针),以确保限制可能浪费的内存量。 59 //用于合并的存储块的大小(maxTinySize)是可调的。当前设置为16字节. 60 //小分配器的主要目标是小字符串和独立的转义变量。在json基准上,分配器将分配数量减少了约12%,并将堆大小减少了约20%。 61 off := c.tinyoffset 62 // 对齐所需(保守)对齐的小指针。调整偏移量。 63 if size&7 == 0 { 64 off = round(off, 8) 65 } else if size&3 == 0 { 66 off = round(off, 4) 67 } else if size&1 == 0 { 68 off = round(off, 2) 69 } 70 //如果剩余空间足够. 当前mcache上绑定的tiny 块内存空间足够,直接分配,并返回 71 if off+size <= maxTinySize && c.tiny != 0 { 72 // 返回指针,调整偏移量为下次分配做好准备。 73 x = unsafe.Pointer(c.tiny + off) 74 c.tinyoffset = off + size 75 c.local_tinyallocs++ 76 mp.mallocing = 0 77 releasem(mp) 78 return x 79 } 80 //当前mcache上的 tiny 块内存空间不足,分配新的maxTinySize块。就是从sizeclass=2的span.freelist获取一个16字节object。 81 span := c.alloc[tinySpanClass] 82 // 尝试从 allocCache 获取内存,获取不到返回0 83 v := nextFreeFast(span) 84 if v == 0 { 85 // 没有从 allocCache 获取到内存,netxtFree函数 尝试从 mcentral获取一个新的对应规格的内存块(新span),替换原先内存空间不足的内存块,并分配内存,后面解析 nextFree 函数 86 v, _, shouldhelpgc = c.nextFree(tinySpanClass) 87 } 88 x = unsafe.Pointer(v) 89 (*[2]uint64)(x)[0] = 0 90 (*[2]uint64)(x)[1] = 0 91 // 对比新旧两个tiny块剩余空间,看看我们是否需要用剩余的自由空间来替换现有的微型块。新块分配后其tinyyoffset = size,因此比对偏移量即可 92 if size < c.tinyoffset || c.tiny == 0 { 93 //用新块替换 94 c.tiny = uintptr(x) 95 c.tinyoffset = size 96 } 97 //消费一个新的完整tiny块 98 size = maxTinySize 99 } else { 100 // 这里开始 正常对象的 内存分配 101 102 // 首先查表,以确定 sizeclass 103 var sizeclass uint8 104 if size <= smallSizeMax-8 { 105 sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv] 106 } else { 107 sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv] 108 } 109 size = uintptr(class_to_size[sizeclass]) 110 spc := makeSpanClass(sizeclass, noscan) 111 //找到对应规格的span.freelist,从中提取object 112 span := c.alloc[spc] 113 // 同小对象分配一样,尝试从 allocCache 获取内存,获取不到返回0 114 v := nextFreeFast(span) 115 116 //没有可用的object。从central获取新的span。 117 if v == 0 { 118 v, span, shouldhelpgc = c.nextFree(spc) 119 } 120 x = unsafe.Pointer(v) 121 if needzero && span.needzero != 0 { 122 memclrNoHeapPointers(unsafe.Pointer(v), size) 123 } 124 } 125 } else { 126 // 这里开始大对象的分配 127 128 // 大对象的分配与 小对象 和普通对象 的分配有点不一样,大对象直接从 mheap 上分配 129 var s *mspan 130 shouldhelpgc = true 131 systemstack(func() { 132 s = largeAlloc(size, needzero, noscan) 133 }) 134 s.freeindex = 1 135 s.allocCount = 1 136 //span.start实际由address >> pageshift生成。 137 x = unsafe.Pointer(s.base()) 138 size = s.elemsize 139 } 140 141 // bitmap标记... 142 // 检查出发条件,启动垃圾回收 ... 143 144 return x 145 }
代码基本思路:
1. 判定对象是大对象、小对象还是微小对象。
2. 如果是微小对象:
直接从 mcache 的alloc 找到对应 classsize 的 mspan;
若当前mspan有足够空间,分配并修改mspan的相关属性(nextFreeFast函数中实现);
若当前mspan的空间不足,则从 mcentral重新获取一块 对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性;
对于微小对象,它不能是指针,因为多个微小对象被组合到一个object里,显然无法应对辣鸡扫描。其次它从span.freelist获取一个16字节的object,然后利用偏移量来记录下一次分配的位置。
3. 如果是小对象,内存分配逻辑大致同微小对象:
首先查表,以确定 需要分配内存的对象的 sizeclass,并找到 对应 classsize的 mspan;
若当前mspan有足够的空间,分配并修改mspan的相关属性(nextFreeFast函数中实现);
若当前mspan没有足够的空间,从 mcentral重新获取一块对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性;
largeAlloc
1 func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan { 2 // print("largeAlloc size=", size, "\n") 3 4 // 内存溢出判断 5 if size+_PageSize < size { 6 throw("out of memory") 7 } 8 9 // 计算出对象所需的页数 10 npages := size >> _PageShift 11 if size&_PageMask != 0 { 12 npages++ 13 } 14 15 // Deduct credit for this span allocation and sweep if 16 // necessary. mHeap_Alloc will also sweep npages, so this only 17 // pays the debt down to npage pages. 18 // 清理(Sweep)垃圾 19 deductSweepCredit(npages*_PageSize, npages) 20 21 // 分配函数的具体实现 22 s := mheap_.alloc(npages, makeSpanClass(0, noscan), true, needzero) 23 if s == nil { 24 throw("out of memory") 25 } 26 s.limit = s.base() + size 27 // bitmap 记录分配的span 28 heapBitsForAddr(s.base()).initSpan(s) 29 return s 30 }
再看看 mheap_.allo()函数的实现:
1 //mheap.go 2 // alloc allocates a new span of npage pages from the GC'd heap. 3 // Either large must be true or spanclass must indicates the span's size class and scannability. 4 // If needzero is true, the memory for the returned span will be zeroed. 5 func (h *mheap) alloc(npage uintptr, spanclass spanClass, large bool, needzero bool) *mspan { 6 // Don't do any operations that lock the heap on the G stack. 7 // It might trigger stack growth, and the stack growth code needs 8 // to be able to allocate heap. 9 //如果needzero为true,则返回范围的内存将为零。 10 //不要执行任何将堆锁定在G堆栈上的操作。 11 //它可能会触发堆栈增长,而堆栈增长代码需要能够分配堆。 12 var s *mspan 13 systemstack(func() { 14 s = h.alloc_m(npage, spanclass, large) 15 }) 16 17 if s != nil { 18 if needzero && s.needzero != 0 { 19 memclrNoHeapPointers(unsafe.Pointer(s.base()), s.npages<<_PageShift) 20 } 21 s.needzero = 0 22 } 23 return s 24 }
mheap.alloc_m()根据页数从 heap 上面分配一个新的span,并且在 HeapMap 和 HeapMapCache 上记录对象的sizeclass。
1 //mheap.go 2 func (h *mheap) alloc_m(npage uintptr, spanclass spanClass, large bool) *mspan { 3 _g_ := getg() 4 if _g_ != _g_.m.g0 { 5 throw("_mheap_alloc not on g0 stack") 6 } 7 lock(&h.lock) 8 9 // 清理垃圾,内存块状态标记 省略... 10 11 // 从 heap中获取指定页数的span 12 s := h.allocSpanLocked(npage, &memstats.heap_inuse) 13 if s != nil { 14 // Record span info, because gc needs to be 15 // able to map interior pointer to containing span. 16 atomic.Store(&s.sweepgen, h.sweepgen) 17 h.sweepSpans[h.sweepgen/2%2].push(s) // Add to swept in-use list.// 忽略 18 s.state = _MSpanInUse 19 s.allocCount = 0 20 s.spanclass = spanclass 21 // 重置span的状态 22 if sizeclass := spanclass.sizeclass(); sizeclass == 0 { 23 s.elemsize = s.npages << _PageShift 24 s.divShift = 0 25 s.divMul = 0 26 s.divShift2 = 0 27 s.baseMask = 0 28 } else { 29 s.elemsize = uintptr(class_to_size[sizeclass]) 30 m := &class_to_divmagic[sizeclass] 31 s.divShift = m.shift 32 s.divMul = m.mul 33 s.divShift2 = m.shift2 34 s.baseMask = m.baseMask 35 } 36 37 // update stats, sweep lists 38 h.pagesInUse += uint64(npage) 39 if large { 40 // 更新 mheap中大对象的相关属性 41 memstats.heap_objects++ 42 mheap_.largealloc += uint64(s.elemsize) 43 mheap_.nlargealloc++ 44 atomic.Xadd64(&memstats.heap_live, int64(npage<<_PageShift)) 45 // Swept spans are at the end of lists. 46 // 根据页数判断是busy还是 busylarge链表,并追加到末尾 47 if s.npages < uintptr(len(h.busy)) { 48 h.busy[s.npages].insertBack(s) 49 } else { 50 h.busylarge.insertBack(s) 51 } 52 } 53 } 54 // gc trace 标记,省略... 55 unlock(&h.lock) 56 return s 57 }
mheap.allocSpanLocked()函数分配一个给定大小的span,并将分配的span从freelist中移除。
//mheap.go 1 func (h *mheap) allocSpanLocked(npage uintptr, stat *uint64) *mspan { 2 var list *mSpanList 3 var s *mspan 4 5 // Try in fixed-size lists up to max. 6 // 先尝试获取指定页数的span,如果没有,则试试页数更多的 7 for i := int(npage); i < len(h.free); i++ { 8 list = &h.free[i] 9 if !list.isEmpty() { 10 s = list.first 11 list.remove(s) 12 goto HaveSpan 13 } 14 } 15 // Best fit in list of large spans. 16 // 从 freelarge 上找到一个合适的span节点返回 ,下面继续分析这个函数 17 s = h.allocLarge(npage) // allocLarge removed s from h.freelarge for us 18 if s == nil { 19 // 如果 freelarge上找不到合适的span节点,就只有从 系统 重新分配了 20 // 后面继续分析这个函数 21 if !h.grow(npage) { 22 return nil 23 } 24 // 从系统分配后,再次到freelarge 上寻找合适的节点 25 s = h.allocLarge(npage) 26 if s == nil { 27 return nil 28 } 29 } 30 31 HaveSpan: 32 // 从 free 上面获取到了 合适页数的span 33 // Mark span in use. 省略.... 34 35 if s.npages > npage { 36 // Trim extra and put it back in the heap. 37 // 创建一个 s.napges - npage 大小的span,并放回 heap 38 t := (*mspan)(h.spanalloc.alloc()) 39 t.init(s.base()+npage<<_PageShift, s.npages-npage) 40 // 更新获取到的span s 的属性 41 s.npages = npage 42 h.setSpan(t.base()-1, s) 43 h.setSpan(t.base(), t) 44 h.setSpan(t.base()+t.npages*pageSize-1, t) 45 t.needzero = s.needzero 46 s.state = _MSpanManual // prevent coalescing with s 47 t.state = _MSpanManual 48 h.freeSpanLocked(t, false, false, s.unusedsince) 49 s.state = _MSpanFree 50 } 51 s.unusedsince = 0 52 // 将s放到spans 和 arenas 数组里面 53 h.setSpans(s.base(), npage, s) 54 55 *stat += uint64(npage << _PageShift) 56 memstats.heap_idle -= uint64(npage << _PageShift) 57 58 //println("spanalloc", hex(s.start<<_PageShift)) 59 if s.inList() { 60 throw("still in list") 61 } 62 return s 63 }
mheap.allocLarge()函数从 mheap 的 freeLarge 树上面找到一个指定page数量的span,并将该span从树上移除,找不到则返回nil。
1 //mheap.go 2 func (h *mheap) allocLarge(npage uintptr) *mspan { 3 // Search treap for smallest span with >= npage pages. 4 return h.freelarge.remove(npage) 5 } 6 7 // 上面的 h.freelarge.remove 即调用这个函数 8 // 典型的二叉树寻找算法 9 func (root *mTreap) remove(npages uintptr) *mspan { 10 t := root.treap 11 for t != nil { 12 if t.spanKey == nil { 13 throw("treap node with nil spanKey found") 14 } 15 if t.npagesKey < npages { 16 t = t.right 17 } else if t.left != nil && t.left.npagesKey >= npages { 18 t = t.left 19 } else { 20 result := t.spanKey 21 root.removeNode(t) 22 return result 23 } 24 } 25 return nil 26 }
mheap.grow()函数在 mheap.allocSpanLocked 这个函数中,如果 freelarge上找不到合适的span节点,就只有从系统重新分配了,那我们接下来就继续分析一下这个函数的实现。
1 func (h *mheap) grow(npage uintptr) bool { 2 ask := npage << _PageShift 3 // 向系统申请内存,后面继续追踪 sysAlloc 这个函数 4 v, size := h.sysAlloc(ask) 5 if v == nil { 6 print("runtime: out of memory: cannot allocate ", ask, "-byte block (", memstats.heap_sys, " in use)\n") 7 return false 8 } 9 10 // Create a fake "in use" span and free it, so that the 11 // right coalescing happens. 12 // 创建 span 来管理刚刚申请的内存 13 s := (*mspan)(h.spanalloc.alloc()) 14 s.init(uintptr(v), size/pageSize) 15 h.setSpans(s.base(), s.npages, s) 16 atomic.Store(&s.sweepgen, h.sweepgen) 17 s.state = _MSpanInUse 18 h.pagesInUse += uint64(s.npages) 19 // 将刚刚申请的span放到 arenas 和 spans 数组里面 20 h.freeSpanLocked(s, false, true, 0) 21 return true 22 }
mheao.sysAlloc()
1 func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { 2 n = round(n, heapArenaBytes) 3 4 // First, try the arena pre-reservation. 5 // 从 arena 中 获取对应大小的内存, 获取不到返回nil 6 v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys) 7 if v != nil { 8 // 从arena获取到需要的内存,跳转到 mapped操作 9 size = n 10 goto mapped 11 } 12 13 // Try to grow the heap at a hint address. 14 // 尝试 从 arenaHint向下扩展内存 15 for h.arenaHints != nil { 16 hint := h.arenaHints 17 p := hint.addr 18 if hint.down { 19 p -= n 20 } 21 if p+n < p { 22 // We can't use this, so don't ask. 23 // 表名 hint.down = false 不能向下扩展内存 24 v = nil 25 } else if arenaIndex(p+n-1) >= 1<<arenaBits { 26 // 超出 heap 可寻址的内存地址,不能使用 27 // Outside addressable heap. Can't use. 28 v = nil 29 } else { 30 // 当前hint可以向下扩展内存,利用mmap向系统申请内存 31 v = sysReserve(unsafe.Pointer(p), n) 32 } 33 if p == uintptr(v) { 34 // Success. Update the hint. 35 if !hint.down { 36 p += n 37 } 38 hint.addr = p 39 size = n 40 break 41 } 42 // Failed. Discard this hint and try the next. 43 // 44 // TODO: This would be cleaner if sysReserve could be 45 // told to only return the requested address. In 46 // particular, this is already how Windows behaves, so 47 // it would simply things there. 48 if v != nil { 49 sysFree(v, n, nil) 50 } 51 h.arenaHints = hint.next 52 h.arenaHintAlloc.free(unsafe.Pointer(hint)) 53 } 54 55 if size == 0 { 56 if raceenabled { 57 // The race detector assumes the heap lives in 58 // [0x00c000000000, 0x00e000000000), but we 59 // just ran out of hints in this region. Give 60 // a nice failure. 61 throw("too many address space collisions for -race mode") 62 } 63 64 // All of the hints failed, so we'll take any 65 // (sufficiently aligned) address the kernel will give 66 // us. 67 v, size = sysReserveAligned(nil, n, heapArenaBytes) 68 if v == nil { 69 return nil, 0 70 } 71 72 // Create new hints for extending this region. 73 hint := (*arenaHint)(h.arenaHintAlloc.alloc()) 74 hint.addr, hint.down = uintptr(v), true 75 hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 76 hint = (*arenaHint)(h.arenaHintAlloc.alloc()) 77 hint.addr = uintptr(v) + size 78 hint.next, mheap_.arenaHints = mheap_.arenaHints, hint 79 } 80 81 // Check for bad pointers or pointers we can't use. 82 { 83 var bad string 84 p := uintptr(v) 85 if p+size < p { 86 bad = "region exceeds uintptr range" 87 } else if arenaIndex(p) >= 1<<arenaBits { 88 bad = "base outside usable address space" 89 } else if arenaIndex(p+size-1) >= 1<<arenaBits { 90 bad = "end outside usable address space" 91 } 92 if bad != "" { 93 // This should be impossible on most architectures, 94 // but it would be really confusing to debug. 95 print("runtime: memory allocated by OS [", hex(p), ", ", hex(p+size), ") not in usable address space: ", bad, "\n") 96 throw("memory reservation exceeds address space limit") 97 } 98 } 99 100 if uintptr(v)&(heapArenaBytes-1) != 0 { 101 throw("misrounded allocation in sysAlloc") 102 } 103 104 // Back the reservation. 105 sysMap(v, size, &memstats.heap_sys) 106 107 mapped: 108 // Create arena metadata. 109 // 根据 v 的address,计算出 arenas 的L1 L2 110 for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ { 111 l2 := h.arenas[ri.l1()] 112 if l2 == nil { 113 // 如果 L2 为 nil,则分配 arenas[L1] 114 // Allocate an L2 arena map. 115 l2 = (*[1 << arenaL2Bits]*heapArena)(persistentalloc(unsafe.Sizeof(*l2), sys.PtrSize, nil)) 116 if l2 == nil { 117 throw("out of memory allocating heap arena map") 118 } 119 atomic.StorepNoWB(unsafe.Pointer(&h.arenas[ri.l1()]), unsafe.Pointer(l2)) 120 } 121 122 // 如果 arenas[ri.L1()][ri.L2()] 不为空 说明已经实例化过了 123 if l2[ri.l2()] != nil { 124 throw("arena already initialized") 125 } 126 var r *heapArena 127 // 从 arena 上分配内存 128 r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys)) 129 if r == nil { 130 r = (*heapArena)(persistentalloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys)) 131 if r == nil { 132 throw("out of memory allocating heap arena metadata") 133 } 134 } 135 136 // Store atomically just in case an object from the 137 // new heap arena becomes visible before the heap lock 138 // is released (which shouldn't happen, but there's 139 // little downside to this). 140 atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r)) 141 } 142 // ... 143 return 144 }
大对象的分配流程至此结束。
3.2 小对象和微小对象的分配
nextFreeFast()函数返回 span 上可用的地址,如果找不到 则返回0
1 func nextFreeFast(s *mspan) gclinkptr { 2 // 计算s.allocCache从低位起有多少个0 3 theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache? 4 if theBit < 64 { 5 6 result := s.freeindex + uintptr(theBit) 7 if result < s.nelems { 8 freeidx := result + 1 9 if freeidx%64 == 0 && freeidx != s.nelems { 10 return 0 11 } 12 // 更新bitmap、可用的 slot索引 13 s.allocCache >>= uint(theBit + 1) 14 s.freeindex = freeidx 15 s.allocCount++ 16 // 返回 找到的内存的地址 17 return gclinkptr(result*s.elemsize + s.base()) 18 } 19 } 20 return 0 21 }
mcache.nextFree()函数。如果 nextFreeFast 找不到 合适的内存,就会进入这个函数。nextFree 如果在cached span 里面找到未使用的object,则返回,否则,调用refill 函数,从 central 中获取对应classsize的span,然后 从新的span里面找到未使用的object返回。
1 //mcache.go 2 func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) { 3 // 先找到 mcache 中 对应 规格的 span 4 s = c.alloc[spc] 5 shouldhelpgc = false 6 // 在 当前span中找到合适的 index索引 7 freeIndex := s.nextFreeIndex() 8 if freeIndex == s.nelems { 9 // The span is full. 10 // freeIndex == nelems 时,表示当前span已满 11 if uintptr(s.allocCount) != s.nelems { 12 println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems) 13 throw("s.allocCount != s.nelems && freeIndex == s.nelems") 14 } 15 // 调用refill函数,从 mcentral 中获取可用的span,并替换掉当前 mcache里面的span 16 systemstack(func() { 17 c.refill(spc) 18 }) 19 shouldhelpgc = true 20 s = c.alloc[spc] 21 22 // 再次到新的span里面查找合适的index 23 freeIndex = s.nextFreeIndex() 24 } 25 26 if freeIndex >= s.nelems { 27 throw("freeIndex is not valid") 28 } 29 30 // 计算出来 内存地址,并更新span的属性 31 v = gclinkptr(freeIndex*s.elemsize + s.base()) 32 s.allocCount++ 33 if uintptr(s.allocCount) > s.nelems { 34 println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems) 35 throw("s.allocCount > s.nelems") 36 } 37 return 38 }
mcache.refill()函数
Refill 根据指定的sizeclass获取对应的span,并作为 mcache的新的sizeclass对应的span
1 //mcache.go 2 func (c *mcache) refill(spc spanClass) { 3 _g_ := getg() 4 5 _g_.m.locks++ 6 // Return the current cached span to the central lists. 7 s := c.alloc[spc] 8 9 if uintptr(s.allocCount) != s.nelems { 10 throw("refill of span with free space remaining") 11 } 12 13 // 判断s是不是 空的span 14 if s != &emptymspan { 15 s.incache = false 16 } 17 // 尝试从 mcentral 获取一个新的span来代替老的span 18 // Get a new cached span from the central lists. 19 s = mheap_.central[spc].mcentral.cacheSpan() 20 if s == nil { 21 throw("out of memory") 22 } 23 24 if uintptr(s.allocCount) == s.nelems { 25 throw("span has no free space") 26 } 27 // 更新mcache的span 28 c.alloc[spc] = s 29 _g_.m.locks-- 30 }
mheap.alloc
3.3 内置函数make() 和 new() 的区别
1 // The new built-in function allocates memory. The first argument is a type, 2 // not a value, and the value returned is a pointer to a newly 3 // allocated zero value of that type. 4 // new内置函数分配内存。 第一个参数是一个类型,而不是一个值,返回的值是一个指向该类型新分配的零值的指针。 5 func new(Type) *Type 6 7 // The make built-in function allocates and initializes an object of type 8 // slice, map, or chan (only). Like new, the first argument is a type, not a 9 // value. Unlike new, make's return type is the same as the type of its 10 // argument, not a pointer to it. The specification of the result depends on 11 // the type: 12 // Slice: The size specifies the length. The capacity of the slice is 13 // equal to its length. A second integer argument may be provided to 14 // specify a different capacity; it must be no smaller than the 15 // length, so make([]int, 0, 10) allocates a slice of length 0 and 16 // capacity 10. 17 // Map: An empty map is allocated with enough space to hold the 18 // specified number of elements. The size may be omitted, in which case 19 // a small starting size is allocated. 20 // Channel: The channel's buffer is initialized with the specified 21 // buffer capacity. If zero, or the size is omitted, the channel is unbuffered. 22 // make 内置函数分配并初始化一个 slice、map 或 chan(仅限)类型的对象。 和 new 一样,第一个参数是一个类型,而不是一个值。 与 new 不同,make 的返回类型与其参数的类型相同,而不是指向它的指针。 结果的规范取决于类型: 23 // 切片:大小指定长度。 切片的容量等于其长度。 可以提供第二个整数参数来指定不同的容量; 它必须不小于长度,因此 make([]int, 0, 10) 分配长度为 0 和容量为 10 的切片。 24 // Map: 一个空的映射被分配了足够的空间来容纳指定数量的元素。 可以省略大小,在这种情况下分配一个小的起始大小。 25 // 通道:通道的缓冲区被初始化为指定的缓冲区容量。 如果为零,或者省略了大小,则通道是无缓冲的。 26 func make(t Type, size ...IntegerType) Type
1. 返回值:
从定义中可以看出,new返回的是指向Type的指针,指向分配的内存地址,该指针可以被隐式地消除引用。 同时 new 函数会把分配的内存置为零,也就是类型的零值。
make直接返回的是Type类型值,一个T类型的结构。
2. 参数:
new只有一个Type参数,Type可以是任意类型数据。 make可以有多个参数,其中第一个参数与new的参数相同,但是只能是slice,map,或者chan中的一种。对于不同类型,size参数说明如下:
- 对于slice,第一个size表示长度,第二个size表示容量,且容量不能小于长度。如果省略第二个size,默认容量等于长度。
- 对于map,会根据size大小分配资源,以足够存储size个元素。如果省略size,会默认分配一个小的起始size。
- 对于chan,size表示缓冲区容量。如果省略size,channel为无缓冲channel。
3.类型:
make 被用来分配引用类型的内存: map, slice, channel
new 被用来分配除了引用类型的所有其他类型的内存: int, string, array,自定义类型等
var sum *int sum = new(int) //分配空间 *sum = 98 fmt.Println(*sum) type Student struct { name string age int } var s *Student s = new(Student) //分配空间 s.name ="dequan" fmt.Println(s)
4.原理
make 在编译期的类型检查阶段,Go语言其实就将代表 make 关键字的 OMAKE 节点根据参数类型的不同转换成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三种不同类型的节点,这些节点最终也会调用不同的运行时函数来初始化数据结构。
new 内置函数 new 会在编译期的 SSA 代码生成阶段经过 callnew 函数的处理,如果请求创建的类型大小是 0,那么就会返回一个表示空指针的 zerobase 变量,在遇到其他情况时会将关键字转换成 newobject。原理如上所述。
主要区别如下:
- make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;
- new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
- new 分配的空间被清零。make 分配空间后,会进行初始化;
4. 总结
1. 判定对象大小:
2. 若是微小对象:
- 从 mcache 的 alloc 找到对应 classsize 的 mspan;
- 当前mspan有足够的空间时,分配并修改mspan的相关属性(nextFreeFast函数中实现);
- 若当前mspan没有足够的空间,从 mcentral 重新获取一块对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性;
- 若 mcentral 没有足够的对应的classsize的span,则去向mheap申请;
- 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配;
- 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中;
3. 若是小对象,内存分配逻辑大致同小对象:
- 查表以确定需要分配内存的对象的 sizeclass,找到 对应classsize的 mspan;
- mspan有足够的空间时,分配并修改mspan的相关属性(nextFreeFast函数中实现);
- 若当前mspan没有足够的空间,从 mcentral重新获取一块对应 classsize的 mspan,替换原先的mspan,然后分配并修改mspan的相关属性;
- 若mcentral没有足够的对应的classsize的span,则去向mheap申请;
- 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配
- 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中
4. 若是大对象,直接从mheap进行分配
- 若对应classsize的span没有了,则找一个相近的classsize的span,切割并分配;
- 若找不到相近的classsize的span,则去向系统申请,并补充到mheap中;