内存管理

 

垃圾回收

“⾮分代的、⾮移动的、并发的、三⾊的”标记清除垃圾回收算法。

整个进程空间⾥申请每个对象占据的内存可以视为⼀个图,初始状态下每个内存对象都是⽩⾊标记。

  1. 做⼀些准备⼯作:收集根对象(全局变量,和G stack),开启写屏障。全局变量、开启写屏障需要STW(暂停所有正在执行的用户线程/协程)。然后取消STW,将扫描任务作为多个并发的goroutine⽴即⼊队给调度器,进⽽被CPU处理。
  2. 第⼀轮先并发扫描root对象(全局指针和 goroutine 栈上的指针),从根出发扫描所有可达对象,标记为灰色,放入待处理队列,并把root标记为黑色。第⼆轮从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。循环往复,最后队列为空时,标记完成后只有黑色和白色对象,黑色代表使用中对象,白色对象代表垃圾,并使用写屏障最终变化的引用关系。
  3. 第三轮再次STW,重新扫描全局变量,和上一轮改变的stack(写屏障),完成标记工作。标记结束阶段的最后会关闭写屏障,然后关闭STW,唤醒熟睡已久的负责清扫垃圾的goroutine。
  4. 清扫goroutine是应用启动后立即创建的一个后台goroutine,它会立刻进入睡眠,等待被唤醒,然后执行垃圾清理:把白色对象挨个清理掉,清扫goroutine和应用goroutine是并发进行的。清扫完成之后,它再次进入睡眠状态,等待下次被唤醒。

GC有3种触发方式:

  1. 辅助GC:在分配内存时,会判断当前的Heap内存分配量是否达到了触发一轮GC的阈值(每轮GC完成后,该阈值会被动态设置),如果超过阈值,则启动一轮GC。
  2. 调用runtime.GC()强制启动一轮GC。
  3. 当超过 forcegcperiod (2分钟)没有运行GC会启动一轮GC。

Dijistra写屏障:满足强三色不变性:黑色节点不允许引用白色节点 当黑色节点新增了白色节点的引用时,将对应的白色节点改为灰色。

混合写屏障:满足弱三色不变性:黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用(确保不会被遗漏),当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色。减少了第二次STW的时间。

内存分配

Go语⾔的运⾏环境(runtime)会在goroutine需要的时候动态地分配栈空间。

分块式的栈:初始分配⼀个8KB的内存空间来给goroutine的栈使⽤。每个Go函数的开头都有⼀⼩段检测代码。这段代码会检查我们是否已经⽤完了分配的栈空间。如果是的话,它会调⽤morestack函数。morestack函数分配⼀块新的内存作为栈空间,并且在这块栈空间的底部填⼊各种信息(包括之前的那块栈地址)。在分配了这块新的栈空间之后,它会重试刚才造成栈空间不⾜的函数。这个过程叫做栈分裂(stack split)。新分配的栈底部,还插⼊了⼀个叫做lessstack的函数指针。当从刚才造成栈空间不⾜的那个函数返回时做准备的。当我们从那个函数返回时,它会跳转到lessstack。lessstack函数会查看在栈底部存放的数据结构⾥的信息,如果新栈已空,然后调整栈指针(stack pointer)。这样就完成了从新的栈块到⽼的栈块的跳转。接下来,新分配的这个块栈空间就可以被释放掉了。

按照需求来扩展和收缩栈的⼤⼩。 热分裂问题:缩减栈空间是⼀个开销相对较⼤的操作。如果在⼀个循环⾥有栈分裂,那么它的开销就变得不可忽略了。⼀个函数会扩展,然后分裂栈。当它返回的时候⼜会释放之前分配的内存块。

栈复制法:当goroutine运⾏并⽤完栈空间的时候,栈溢出检查会被触发。会分配⼀个两倍⼤的内存块并把⽼的内存块内容复制到新的内存块⾥。这样做意味着当栈缩减回之前⼤⼩时,我们不需要做任何事情。栈的缩减没有任何代价。⽽且,当栈再次扩展时,运⾏环境也不需要再做任何事。它可以重⽤之前分配的空间。

存储在栈上的变量的地址可能已经被使⽤到。也就是说程序使⽤到了⼀些指向栈的指针。当移动栈的时候,所有指向栈⾥内容的指针都会变得⽆效。当我们移动栈的时候,我们可以更新栈⾥的指针让它们指向新的地址,使⽤了垃圾回收的引用关系信息来复制栈。

逃逸

1,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

逃逸分析的好处应该是尽量将变量分配到栈上,栈的分配比堆快,性能好,可以避免 Go 频繁地进行垃圾回收。

2,编译器在编译阶段完成,根据变量是否被外部引用来决定是否逃逸:

内存管理

1,结构

image-20220817212827790

2,Go通过细致的对象划分、极致的多级缓存+无锁策略缓存、精确的位图管理来进行精细化的内存管理和性能保障。Go中把所有对象分为三个层级:

3,mspan 是一分配内存的单位。,mcache、mcentral、mheap 起到了内存池的作用,会被预分配内存,当有对应大小的对象需要分配时会先到它们这一层请求。如果这一层内存池不够用时,会按照下面的顺序一层一层的往上申请内存:mcache -> mcentral-> mheap -> 操作系统。

image-20220818110446803