Go内存管理

Go内存管理 - 图1

Go内存管理基于TCMalloc,使用连续虚拟地址,以页(8k)为单位、多级缓存进行管理;在分配内存时,需要对size进行对齐处理,根据best-fit找到合适的mspan,对未用完的内存还会拆分成其他大小的mspan继续使用。

在new一个object时(忽略逃逸分析),根据object的size做不同的分配策略:

  • 极小对象(size<16byte)直接在当前P的mcache上的tiny缓存上分配;
  • 小对象(16byte <= size <= 32k)在当前P的mcache上对应slot的空闲列表中分配,无空闲列表则会继续向mcentral申请(还是没有则向mheap申请);
  • 大对象(size>32k)直接通过mheap申请。

1. 内存分配知识

  • 计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址(PA)
  • 现代处理器使用的是一种为虚拟寻址(VA)的寻址形式,最少的寻址单位是字
  • 虚拟地址映射物理地址是通过读取页表(page table)进行地址翻译完成的:页表存放在物理存储器中
  • MMU(内核吧物理页作为内存管理的基本单位)以页(page)大小为单位来管理系统中的页表
  • 在虚拟存储器的习惯说法中,DRAM缓存不命中的成为:缺页
  • page的结构与物理页相关,而非与虚拟页相关
  • 系统中的每个物理页都要分配一个page结构体

在了解Go的内存分配器原理之前,我们先了解一下“动态存储分配器”。

2. 动态存储分配器

动态存储分配器维护着一个进程的虚拟存储区域,这个区域称为 “堆”,堆可以视为一组大小不同的 “块”(chunk: 连续的虚拟存储片,无论内存分配器和垃圾回收算法都依赖连续地址)的集合,并交由动态存储器维护。

动态分配器主要分为:

  • 显式:常见的malloc
  • 隐式:垃圾回收

在Go里头,分配器将其管理(大块 —> 小块)的内存块分为两种:

  • span:由多个连续的页(page)组成的大块内存
  • object:将span按特定大小切分多个小块,每个小块可存储一个对象

按照其用途,span面向内部管理,object面向对象分配.

分配器按页数来区分不同大小的span。比如,以页数为单位将span存放到管理数组中,需要时就以页数为索引进行查找。当然,span大小并非固定不变。在获取闲置span时,如果没找到大小合适的,那就返回页数更多的,此时会引发裁剪操作,多余部分将构成新的span被放回管理数组。分配器还会尝试将地址相邻的空闲span合并,以构建更大的内存块,减少碎片,提供更灵活的分配策略。

我们在go的malloc.go代码中:

  1. const (
  2. _PageShift = 13
  3. _PageSize = 1 << _PageShift
  4. _PageMask = _PageSize - 1
  5. )

可以看到,用于存储对象的object,按照8字节倍数分为n种。这种方式虽然会造成一些内存浪费,但分配器只须面对有限的几种规格的小块内存,优化了分配和复用管理策略。

  1. 分配器会尝试将多个微小对象组合到一个object快内,以节约内存。

我们看到msize.go的部分:

  1. var class_to_size [_NumSizeClasses]int32
  2. var class_to_allocnpages [_NumSizeClasses]int32
  3. var class_to_divmagic [_NumSizeClasses]divMagic
  4. var size_to_class8 [1024/8 + 1]int8
  5. var size_to_class128 [(_MaxSmallSize-1024)/128 + 1]int8
  6. func sizeToClass(size int32) int32 {
  7. if size > _MaxSmallSize {
  8. throw("invalid size")
  9. }
  10. if size > 1024-8 {
  11. return int32(size_to_class128[(size-1024+127)>>7])
  12. }
  13. return int32(size_to_class8[(size+7)>>3])
  14. }

分配器初始化时,会构建对照表存储大小和规格对应关系,包括用来切分的span页数。

  1. // Tunable constants.
  2. _MaxSmallSize = 32 << 10

从上面的代码段,我们大概可以指定若对象大小超出特定阈值限制,会被当做大对象特别对待。

3. mmap函数

Unix进程可以使用mmap函数来创建新的虚拟存储区域并将对象映射到这些区域中。

mmap函数要求内核创建一个新的虚拟存储区域,最好是从起始地址start开始的一个区域,并将文件描述符fd指定的对象的一个连续的片(chunk)映射到新的区域。

4. 数据频繁分配与回收

对于有效地进行数据频繁分配与回收,减少碎片,一般有两种手段:

  • 空闲链表: 提供直接可供使用,已分配的结构块,缺点是不能全局控制.
  • slab:linux提供的,可以把不同的对象划分为所谓高速缓存组.

5. Go的内存分配

Go的内存分配器是采用google自家的tcmalloc,tcmalloc是一个带内存池的分配器,底层直接调用mmap函数,并使用bestfit进行动态分配。

Go为每个系统线程分配了一个本地MCache,少量的地址分配就是从MCache分配的,并且定期进行垃圾回收,所以可见go的分配器包含了显式与隐式的调用。

Go定义的小块内存,大小上是指32K或以下的对象,go底层会把这些小块内存按照指定规格(大约100种)进行切割:为了避免随意切割,申请任意字节内存时会向上取整到接近的块,将整块分配(从空闲链表)给到申请者。

Go内存分配主要组件:

  • MCache:层次与MHeap类似,对于每个尺寸的类别都有一个空闲链表。每个M都有自己的局部Mcache(小对象从它取,无需加锁),这就是Go能够在多线程中高效内存管理的重要原因.

  • MCentral:在无空闲内存的时候,向Mheap申请一个span,而不是多个,申请的span包含多少个page由central的sizeclass来确定(跨进程复用)

  • MHeap:负责将MSpan组织和管理起来。

(1). 分配过程:从free[]中分配,如果发生切割则将剩余的部分放回到free[]中.

(2). 回收过程:回收一个Mspan时,首选查找它相邻的地址,再通过map映射得到对应的Mspan,如果Mspan的state是未使用,则可以将 两者进行合并。最后将这页或者合并后的页归还到free[]分配池或者large中。

6. Go的内存模型

Go的内存模型可以视为两级的内存模型:

第一级:Mheap为主要组件:分配的单位是页,但管理的单位是MSpan,每次分配都是用bestFit的原则分配连续的页,回收是采用位图的方式。

第二级:MCache为主要组件:相当于一个内存池, 回收采用引用计数器.

分配场景:

为对象分配内存须区分是在栈上分配还是在堆上分配。通常情况下,编译器有责任尽可能使用寄存器和栈来存储对象,这有助于提升性能,减少垃圾回收器的压力。

应用示例:

  1. package main
  2. func patent() *int {
  3. x := new(int)
  4. *x = 1234
  5. return x
  6. }
  7. func main() {
  8. println(*patent())
  9. }

我们禁止内联优化来编译上面的代码:

  1. > go build -gcflags="-l" -o patent main.go

得到的结果是:

  1. main.go:3 0x2040 65488b0c25a0080000 GS MOVQ GS:0x8a0, CX
  2. main.go:3 0x2049 483b6110 CMPQ 0x10(CX), SP
  3. main.go:3 0x204d 7639 JBE 0x2088
  4. main.go:3 0x204f 4883ec18 SUBQ $0x18, SP
  5. main.go:3 0x2053 48896c2410 MOVQ BP, 0x10(SP)
  6. main.go:3 0x2058 488d6c2410 LEAQ 0x10(SP), BP
  7. main.go:4 0x205d 488d05dc3b0500 LEAQ 0x53bdc(IP), AX
  8. main.go:4 0x2064 48890424 MOVQ AX, 0(SP)
  9. main.go:4 0x2068 e823a70000 CALL runtime.newobject(SB)
  10. main.go:4 0x206d 488b442408 MOVQ 0x8(SP), AX
  11. main.go:5 0x2072 48c700d2040000 MOVQ $0x4d2, 0(AX)
  12. main.go:6 0x2079 4889442420 MOVQ AX, 0x20(SP)
  13. main.go:6 0x207e 488b6c2410 MOVQ 0x10(SP), BP
  14. main.go:6 0x2083 4883c418 ADDQ $0x18, SP
  15. main.go:6 0x2087 c3 RET
  16. main.go:3 0x2088 e893720400 CALL runtime.morestack_noctxt(SB)
  17. main.go:3 0x208d ebb1 JMP main.patent(SB)
  18. :-1 0x208f cc INT $0x3

从结果的 CALL runtime.newobject(SB) ,证明我们的对象在堆上进行分配了。

但当使用默认参数,我们观察下结果:

  1. > go build -o patent main.go

当我们跟上面一样分析test的分配情况时:

  1. > go tool objdump -s "main\.patent" patent

命令执行后,并没有输出, 我们分析下main方法:

  1. > go tool objdump -s "main\.main" patent

得到的结果如下:

  1. main.go:9 0x2040 65488b0c25a0080000 GS MOVQ GS:0x8a0, CX
  2. main.go:9 0x2049 483b6110 CMPQ 0x10(CX), SP
  3. main.go:9 0x204d 763d JBE 0x208c
  4. main.go:9 0x204f 4883ec18 SUBQ $0x18, SP
  5. main.go:9 0x2053 48896c2410 MOVQ BP, 0x10(SP)
  6. main.go:9 0x2058 488d6c2410 LEAQ 0x10(SP), BP
  7. main.go:10 0x205d 48c7442408d2040000 MOVQ $0x4d2, 0x8(SP)
  8. main.go:10 0x2066 e875210200 CALL runtime.printlock(SB)
  9. main.go:10 0x206b 48c70424d2040000 MOVQ $0x4d2, 0(SP)
  10. main.go:10 0x2073 e8b8280200 CALL runtime.printint(SB)
  11. main.go:10 0x2078 e8e3230200 CALL runtime.printnl(SB)
  12. main.go:10 0x207d e8ee210200 CALL runtime.printunlock(SB)
  13. main.go:11 0x2082 488b6c2410 MOVQ 0x10(SP), BP
  14. main.go:11 0x2087 4883c418 ADDQ $0x18, SP
  15. main.go:11 0x208b c3 RET
  16. main.go:9 0x208c e83f720400 CALL runtime.morestack_noctxt(SB)
  17. main.go:9 0x2091 ebad JMP main.main(SB)

这表明内联优化后的代码没有调用newobject在堆上分配内存。

编译器这么做的目的是:没有内联时,需要在两个栈帧间传递对象,因此在堆上分配而不是返回一个失效栈帧的数据。而当内联后,实际上就成看main栈帧内的局部变量,无需到堆上操作。

内存分配流程:

1、将小对象的大小向上取整到一个对应的尺寸类别(大约100种),查找相应的MCache的空闲链表,如果链表不空,直接从上面分配一个对象,这个过程不加锁

2、如果MCache自由链表是空的,通过MCentral的自由链表取一些对象进行补充

3、如果MCentral的自由链表是空的,则往MHeap中取用一些页对MCentral进行补充,然后将这些内存截断成特定规格

4、如果MHeap空或者没有足够大的页的情况下,从操作系统分配一组新的页面,一般在1MB以上

Go分配流程核心源码实现:

  1. func mallocgc(size uintptr, typ *_type, flags uint32) unsafe.Pointer {
  2. if gcphase == _GCmarktermination {
  3. throw("mallocgc called with gcphase == _GCmarktermination")
  4. }
  5. if size == 0 {
  6. return unsafe.Pointer(&zerobase)
  7. }
  8. if flags&flagNoScan == 0 && typ == nil {
  9. throw("malloc missing type")
  10. }
  11. if debug.sbrk != 0 {
  12. align := uintptr(16)
  13. if typ != nil {
  14. align = uintptr(typ.align)
  15. }
  16. return persistentalloc(size, align, &memstats.other_sys)
  17. }
  18. // assistG is the G to charge for this allocation, or nil if
  19. // GC is not currently active.
  20. var assistG *g
  21. if gcBlackenEnabled != 0 {
  22. // Charge the current user G for this allocation.
  23. assistG = getg()
  24. if assistG.m.curg != nil {
  25. assistG = assistG.m.curg
  26. }
  27. // Charge the allocation against the G. We'll account
  28. // for internal fragmentation at the end of mallocgc.
  29. assistG.gcAssistBytes -= int64(size)
  30. if assistG.gcAssistBytes < 0 {
  31. // This G is in debt. Assist the GC to correct
  32. // this before allocating. This must happen
  33. // before disabling preemption.
  34. gcAssistAlloc(assistG)
  35. }
  36. }
  37. // Set mp.mallocing to keep from being preempted by GC.
  38. mp := acquirem()
  39. if mp.mallocing != 0 {
  40. throw("malloc deadlock")
  41. }
  42. if mp.gsignal == getg() {
  43. throw("malloc during signal")
  44. }
  45. mp.mallocing = 1
  46. shouldhelpgc := false
  47. dataSize := size
  48. c := gomcache()
  49. var s *mspan
  50. var x unsafe.Pointer
  51. if size <= maxSmallSize {
  52. if flags&flagNoScan != 0 && size < maxTinySize {
  53. //小对象分配
  54. off := c.tinyoffset
  55. // Align tiny pointer for required (conservative) alignment.
  56. if size&7 == 0 {
  57. off = round(off, 8)
  58. } else if size&3 == 0 {
  59. off = round(off, 4)
  60. } else if size&1 == 0 {
  61. off = round(off, 2)
  62. }
  63. if off+size <= maxTinySize && c.tiny != 0 {
  64. // The object fits into existing tiny block.
  65. x = unsafe.Pointer(c.tiny + off)
  66. c.tinyoffset = off + size
  67. c.local_tinyallocs++
  68. mp.mallocing = 0
  69. releasem(mp)
  70. return x
  71. }
  72. // Allocate a new maxTinySize block.
  73. s = c.alloc[tinySizeClass]
  74. v := s.freelist
  75. if v.ptr() == nil {
  76. systemstack(func() {
  77. c.refill(tinySizeClass)
  78. })
  79. shouldhelpgc = true
  80. s = c.alloc[tinySizeClass]
  81. v = s.freelist
  82. }
  83. s.freelist = v.ptr().next
  84. s.ref++
  85. // prefetchnta offers best performance, see change list message.
  86. prefetchnta(uintptr(v.ptr().next))
  87. x = unsafe.Pointer(v)
  88. (*[2]uint64)(x)[0] = 0
  89. (*[2]uint64)(x)[1] = 0
  90. // See if we need to replace the existing tiny block with the new one
  91. // based on amount of remaining free space.
  92. if size < c.tinyoffset || c.tiny == 0 {
  93. c.tiny = uintptr(x)
  94. c.tinyoffset = size
  95. }
  96. size = maxTinySize
  97. } else {
  98. var sizeclass int8
  99. if size <= 1024-8 {
  100. sizeclass = size_to_class8[(size+7)>>3]
  101. } else {
  102. sizeclass = size_to_class128[(size-1024+127)>>7]
  103. }
  104. size = uintptr(class_to_size[sizeclass])
  105. s = c.alloc[sizeclass]
  106. v := s.freelist
  107. if v.ptr() == nil {
  108. systemstack(func() {
  109. c.refill(int32(sizeclass))
  110. })
  111. shouldhelpgc = true
  112. s = c.alloc[sizeclass]
  113. v = s.freelist
  114. }
  115. s.freelist = v.ptr().next
  116. s.ref++
  117. // prefetchnta offers best performance, see change list message.
  118. prefetchnta(uintptr(v.ptr().next))
  119. x = unsafe.Pointer(v)
  120. if flags&flagNoZero == 0 {
  121. v.ptr().next = 0
  122. if size > 2*sys.PtrSize && ((*[2]uintptr)(x))[1] != 0 {
  123. memclr(unsafe.Pointer(v), size)
  124. }
  125. }
  126. }
  127. } else {
  128. var s *mspan
  129. shouldhelpgc = true
  130. systemstack(func() {
  131. s = largeAlloc(size, flags)
  132. })
  133. x = unsafe.Pointer(uintptr(s.start << pageShift))
  134. size = s.elemsize
  135. }
  136. if flags&flagNoScan != 0 {
  137. // All objects are pre-marked as noscan. Nothing to do.
  138. } else {
  139. if typ == deferType {
  140. dataSize = unsafe.Sizeof(_defer{})
  141. }
  142. heapBitsSetType(uintptr(x), size, dataSize, typ)
  143. if dataSize > typ.size {
  144. // Array allocation. If there are any
  145. // pointers, GC has to scan to the last
  146. // element.
  147. if typ.ptrdata != 0 {
  148. c.local_scan += dataSize - typ.size + typ.ptrdata
  149. }
  150. } else {
  151. c.local_scan += typ.ptrdata
  152. }
  153. publicationBarrier()
  154. }
  155. if gcphase == _GCmarktermination || gcBlackenPromptly {
  156. systemstack(func() {
  157. gcmarknewobject_m(uintptr(x), size)
  158. })
  159. }
  160. if raceenabled {
  161. racemalloc(x, size)
  162. }
  163. if msanenabled {
  164. msanmalloc(x, size)
  165. }
  166. mp.mallocing = 0
  167. releasem(mp)
  168. if debug.allocfreetrace != 0 {
  169. tracealloc(x, size, typ)
  170. }
  171. if rate := MemProfileRate; rate > 0 {
  172. if size < uintptr(rate) && int32(size) < c.next_sample {
  173. c.next_sample -= int32(size)
  174. } else {
  175. mp := acquirem()
  176. profilealloc(mp, x, size)
  177. releasem(mp)
  178. }
  179. }
  180. if assistG != nil {
  181. // Account for internal fragmentation in the assist
  182. // debt now that we know it.
  183. assistG.gcAssistBytes -= int64(size - dataSize)
  184. }
  185. if shouldhelpgc && gcShouldStart(false) {
  186. gcStart(gcBackgroundMode, false)
  187. }
  188. return x
  189. }

Go也有happens-before ,go happens-before常用的三原则是:

  • 对于不带缓冲区的channel,对其写happens-before对其读.
  • 对于带缓冲区的channel,对其读happens-before对其写.
  • 对于不带缓冲的channel的接收操作 happens-before 相应channel的发送操作完成.