# 16. 内存模型

image-20220904114448954

总结

内存结构:

  • Go 将堆内存抽象为 mheap 结构体;
  • Go 进程会从虚拟内存中申请 n 个 heapArena
  • 每个 heapArena 被按需划分成不同 class 的 mspan,class 有 68 个等级;
  • 每个 mspan 由 n 个相同大小的 span 组成;
  • 为了快速寻找到对应 span,为 mheap 建立了 136 个中央索引 mcentral
  • 每个 mcentral 存储每种 class 的 mspan,每种 mspan 又划分为 gc scan 和 no scan 两种,故共有 68 × 2 = 136 个 mcentral;
  • 为了解决中央索引在并发性的锁竞争问题,为每一个 p(thread)建立一个本地缓存 mcache
  • 每个 mcache 存储 128 个 span,分别是每种 class 的 mspan 的一个 scan 和 noscan 的 span;

内存分配:

  • Go 中将根据对象大小分为 tiny、small 和 large 三种对象;
  • tiny 和 small 对象会被分配到 class1 ~ class67 的 span 中;
  • large 对象会量身定做分配到 class0 的 span 中,直接从 mheap 上申请;
  • 为对象分配内存时,会先从 mcache 上找 span,找不到就去 mcentral 上交换,还找不到,就去 mheap 上申请,还找不到,就 OOM。

# 16.1 协程栈

# 16.1.1 作用

  • 记录协程的执行路径
  • 记录局部变量
  • 函数传参
  • 函数返回值

# 16.1.2 位置

  • Go 协程栈位于 Go 堆内存上
  • Go 堆内存位于操作系统虚拟内存上

# 16.1.3 图解

package main

func sum(a, b int) int {
  sum := 0
  sum = a + b
  return sum
}

func main() {
  a := 3
  b := 5
  print(sum(a, b))
}

image-20220903163506814

# 16.1.4 参数传递

  • Go 是值传递
  • 传递结构体时:会拷贝结构体中的全部内容
  • 传递结构体指针时:会拷贝结构体指针

# 16.1.5 栈大小

默认 2KB

# 16.1.6 逃逸分析

不是所有的变量都能放在协程栈上,例如栈帧回收后需要继续使用的变量和太大的变量,都会逃逸到 上。

具体有三种逃逸:

  • 指针逃逸:函数返回对象的指针
  • 空接口逃逸:函数参数为 interface{},那么函数的实参很可能会逃逸,因为底层很可能会用到反射
  • 大变量逃逸:变量太大,栈帧放不下,就逃逸到堆上了。64 位机器中,一般超过 64KB 的变量就会逃逸

# 16.1.7 栈扩容

Go 栈的初始空间为 2KB。在函数调用前会执行 morestack 判断栈空间是否足够,如果不够,就需要对栈进行扩容。

  • 分段栈:Go1.13 之前
    • 没有空间浪费,但是栈帧会在不连续的空间之间横跳,性能较差。
  • 连续栈:Go1.13 及之后
    • 开辟一整块新的大栈,然后把老的拷贝过来,保证了空间一直连续。但是伸缩时的开销较大。

# 16.2 虚拟内存单元 heapArena

# 16.2.1 概述

  • 物理内存为 64G 的机器中,每个 Go 进程可以会被分配到 256TB 的虚拟内存。
  • Go 的虚拟内存单元为 heapArena,每次申请 64M。
  • 最多可以申请 220 个。
  • 所有的 heapArena 组成了 mheap(Go 堆内存)

image-20220904105731381

PS:操作系统 - 虚拟内存 (opens new window)

# 16.2.2 底层

type heapArena struct {
   bitmap [heapArenaBitmapBytes]byte
   spans [pagesPerArena]*mspan				  // heapArena 会被分成很多个 mspan
   pageInUse [pagesPerArena / 8]uint8		// 哪些页正在被使用中
   pageMarks [pagesPerArena / 8]uint8
   pageSpecials [pagesPerArena / 8]uint8
   checkmarks *checkmarksMap
   zeroedBase uintptr
}

# 16.2.3 分配策略

线性分配 链表分配 分级分配
image-20220904110854548 image-20220904110824878 image-20220904111053653
实现简单,但内存碎片较多。 将空闲块连接起来,牺牲了一部分性能,来能缓解一部分的内存碎片。 将 heapArena 按级别分成很多个块,根据对象大小存放在能容纳它的最小的块中。
Go 参考了分级分配策略,将每一个级定义为 mspanmspan 为 N 个相同大小 span 组成

PS:分级分配参考了 TCMalloc (opens new window)

# 16.3 内存管理单元 mspan

# 16.3.1 概述

  • Go 使用内存时的最小单位是 mspan
  • 每个 mspan 为 N 个相同大小的 span 组成。
  • Go 中一个有 67mspan
// runtime/sizeclasses.go

// class  bytes/obj  bytes/span  objects  tail waste  max waste
//     1          8        8192     1024           0     87.50%
//     2         16        8192      512           0     43.75%
//     3         24        8192      341           8     29.24%
//                                ...
//    62      20480       40960        2           0      6.87%
//    63      21760       65536        3         256      6.25%
//    64      24576       24576        1           0     11.45%
//    65      27264       81920        3         128     10.00%
//    66      28672       57344        2           0      4.91%
//    67      32768       32768        1           0     12.50%

image-20220904111927956

# 16.3.2 底层

type mspan struct {
   next *mspan     								// 下一个 span	
   prev *mspan     								// 上一个 span
    ...
   spanclass   spanClass     			// 级别
		...  
}

# 16.4 中心索引 mcentral

# 16.4.1 概述

heapArena 中的 mspan 不是一开始就全部划分好的,而且按需划分。

由于每个 heapArena 中的 mspan 都是不确定的,为了给要分配空间的对象快速定位到合适的 mspan,Go 中定义了中心索引 mcentral

总共有 136mcentral 结构体,其中 68 个需要 gc 扫描的 mspan,68 个不需要 gc 的 mspan。

image-20220904113242511

# 16.4.2 底层

type mcentral struct {
   spanclass spanClass		// class 级别
   partial [2]spanSet 		// 未满的 span
   full    [2]spanSet 		// 满的 span
}

# 16.5 线程缓存 mcache

# 16.5.1 概述

mcentral 实际是一个中心索引,修改它需要使用互斥锁进行保护,锁冲突会造成性能问题。Go 参考 GMP 模型,增加线程本地缓存。给每一个线程做了一个二级缓存 mcache,极大缓解了并发锁争夺的性能消耗。

  • 每个 P 有一个 mcahce
  • 对于每一种 class 的 mcentral,取一个 scan 和一个 noscan,这样一个 mcache 拥有 136mspan。其中 68 个需要 GC 扫描,68 个不需要。
  • 当本地索引用完了之后,就上锁,去中央索引进行交换。

image-20220904114448954

# 16.5.2 底层

type mcache struct {
   nextSample uintptr 
   scanAlloc  uintptr 
   tiny       uintptr
   tinyoffset uintptr
   tinyAllocs uintptr
   alloc [numSpanClasses]*mspan  // 136 个 msapn
   stackcache [_NumStackOrders]stackfreelist
   flushGen uint32
}
type p struct {
	 ...
   mcache      *mcache
	 ...
}

# 16.6 堆 mheap

最后,我们来看看 Go 中对堆内存的抽象结构:mheap

type mheap struct {
	// 锁
	lock      mutex
  
	// mspan 是按需分级的,这里保存了所有一个划分好的 mspan
	allspans []*mspan 
  
	// mheap 被划分为多个 heapArena
	arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena

	 // 中心索引 
	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
	}
  
   // 其他省略
   ...
}

# 16.7 内存分配

# 16.7.1 对象分级

  • Tiny(0 ~ 16B) :微对象,无指针
  • Small(16B~32KB):小对象
  • Large(32KB,+∞):大对象

TinySmall 会被分配到普通的 mspan(class1 ~ class67)

Large 会被分配到量身定做的 mspan(class0)

# 16.7.2 微对象分配

  1. 从 mcache 拿到 mspan-class2;
  2. 将多个 tiny 合并成一个 16Bytes 存入;
  3. 如果 mcache 中没有空闲的 span,那就去 mcentral 兑换新的 span;
  4. 如果 mcentral 中还没,那就去 mheap 上申请;
image-20220904151512726

# 16.7.3 小对象分配

  1. 根据对象大小确定 mspan 的 class;
  2. 在 mcache 中寻找对应 class 的 span 进行分配;
  3. 如果 mcache 中没有空闲的 span,那就去 mcentral 兑换新的 span;
  4. 如果 mcentral 中还没,那就去 mheap 上申请;

# 16.7.4 大对象分配

量身定做 class0,直接从 mheap 上申请内存。

// allocLarge allocates a span for a large object.
func (c *mcache) allocLarge(size uintptr, needzero bool, noscan bool) *mspan {
	 // 1. 检查 OOM,对象太大了顶不住 
  if size+_PageSize < size {
		throw("out of memory")
	 }
   // 2. 检查大对象需要多少 page
	 npages := size >> _PageShift
	 if size&_PageMask != 0 {
		 npages++
	 }
   // 3. 大对象统一分配到 class0
   spc := makeSpanClass(0, noscan)
   // 4. 直接从 mheap 中申请 class0 的 span
   s := mheap_.alloc(npages, spc, needzero)
   ...
   // 5. 申请完了之后,再将该 span 的索引建立到 mcentral 上,方便 gc 扫描
   mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)
   s.limit = s.base() + size
   heapBitsForAddr(s.base()).initSpan(s)
   return s
}

# 16.7.5 mcache 替换

  • 在 mcache 中,每个 class 的 mspan 只有一个,当 mspan 满了之后,会从 mcentral 中兑换一个新的。
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
	s = c.alloc[spc]
	shouldhelpgc = false
	freeIndex := s.nextFreeIndex()
	if freeIndex == s.nelems {
		// mcache 中的 span 满了,要去 mcentral 中兑换一个新的
		c.refill(spc)
		shouldhelpgc = true
		s = c.alloc[spc]

		freeIndex = s.nextFreeIndex()
	}
	...
}
func (c *mcache) refill(spc spanClass) {
   // 1. 将当前 mcache 中的 span 释放回 mcentral
   s := c.alloc[spc]
   if s != &emptymspan {
      if s.sweepgen != mheap_.sweepgen+3 {
         throw("bad sweepgen in refill")
      }
      mheap_.central[spc].mcentral.uncacheSpan(s)
   }

   // 2. 从 mcentral 中获得一个新的 span
   s = mheap_.central[spc].mcentral.cacheSpan()
	...
}

# 16.7.6 mcentral 扩容

  • mcentral 中,只有有限数量的 mspan,当 mspan 缺少时,会像 mheap 中开辟新的 heapArena,并申请对应 class 的 span。
// Allocate a span to use in an mcache.
func (c *mcentral) cacheSpan() *mspan {
   ...
   // 1. mcache 想从 mcentral 中兑换新的span,
   // 发现 mcentral 已经没有可用的 span 了
   // 那就只能开辟新的 heapArena,并申请对应 class 的 span 了
   s = c.grow()
   if s == nil {
      return nil
   }
	 ...
   return s
}
// grow allocates a new empty span from the heap and initializes it for c's size class.
func (c *mcentral) grow() *mspan {
   npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
   size := uintptr(class_to_size[c.spanclass.sizeclass()])
	 
   // 在 mheap 中分配新的 heapArena 和申请对应 class 的 span
   s := mheap_.alloc(npages, c.spanclass, true)
   if s == nil {
      return nil
   }
   n := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2
   s.limit = s.base() + size*n
   heapBitsForAddr(s.base()).initSpan(s)
   return s
}

# 16.7.7 mallocgc 源码分析

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {

	 ...
   var span *mspan
   var x unsafe.Pointer
   noscan := typ == nil || typ.ptrdata == 0
  
   if size <= maxSmallSize {
			// ① tiny 微对象
      if noscan && size < maxTinySize {
         // 1. 合并多个微对象,尽可能到 16B
         off := c.tinyoffset
         if size&7 == 0 {
            off = alignUp(off, 8)
         } else if sys.PtrSize == 4 && size == 12 {
            off = alignUp(off, 8)
         } else if size&3 == 0 {
            off = alignUp(off, 4)
         } else if size&1 == 0 {
            off = alignUp(off, 2)
         }
         if off+size <= maxTinySize && c.tiny != 0 {
            x = unsafe.Pointer(c.tiny + off)
            c.tinyoffset = off + size
            c.tinyAllocs++
            mp.mallocing = 0
            releasem(mp)
            return x
         }
         // 2. 在 mcache 中找到分配给当前线程的 span
         span = c.alloc[tinySpanClass]
         // 3. 尝试获取 mcache 中的 span
         v := nextFreeFast(span)
         if v == 0 {
            // 4. 如果 mcache 中对应 class 的 span 已经被分配了
            //     就去中央索引中交换空闲的 span
            v, span, shouldhelpgc = c.nextFree(tinySpanClass)
         } 
         // 5. 分配内存
         x = unsafe.Pointer(v)
         (*[2]uint64)(x)[0] = 0
         (*[2]uint64)(x)[1] = 0
         if size < c.tinyoffset || c.tiny == 0 {
            c.tiny = uintptr(x)
            c.tinyoffset = size
         }
         size = maxTinySize
      } else {
         // ② small 小对象
         var sizeclass uint8
         // 1. 根据对象大小去查表,找到对应 class 的级别
         if size <= smallSizeMax-8 {
            sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
         } else {
            sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
         }
         size = uintptr(class_to_size[sizeclass])
         // 2. 确定 mspan 的 class
         spc := makeSpanClass(sizeclass, noscan)
         // 3. 在 mcache 中找到分配给当前线程的 span
         span = c.alloc[spc]
         // 4. 尝试获取 mcache 中的 span
         v := nextFreeFast(span)
         if v == 0 {
            // 5. 如果 mcache 中对应 class 的 span 已经被分配了
            // 就去中央索引换空闲的 span 来分配
            v, span, shouldhelpgc = c.nextFree(spc)
         }
         // 6. 分配内存
         x = unsafe.Pointer(v)
         if needzero && span.needzero != 0 {
            memclrNoHeapPointers(unsafe.Pointer(v), size)
         }
      }
   } else {
      // ③ large 大对象
      shouldhelpgc = true
      // 针对大对象量身定制一个 class0 的 span
      span = c.allocLarge(size, needzero, noscan)
      span.freeindex = 1
      span.allocCount = 1
      x = unsafe.Pointer(span.base())
      size = span.elemsize
   }
	 ...
   return x
}
上次更新: 9/4/2022, 4:29:43 PM