内存管理和编译优化 | 青训营笔记

171 阅读5分钟

这是我参与[第三届青训营-后端场]笔记创作活动的第1篇笔记

自动内存管理技术

- 追踪垃圾回收

回收对象:不可达的对象。 回收开始时会扫描gc roots,例如全局变量,栈的对象,静态变量,常量等。 从gc roots出发,沿着指针追踪扫描所有可达对象。 扫描结束后回收不可达对象。

  • go的GC机制
  • go的GC机制是无分代,不整理(无拷贝移动),并发的三色标记清扫算法:
  1. 对象整理是为了避免内存碎片问题,go的内存分配采用tcmalloc,可以有效的减少内存碎片。
  2. go的编译器会通过逃逸分析把不会逃逸的大部分新生变量分配到栈上,这些变量会被栈直接回收,不需要通过GC回收,而长期生存的变量一般都分配到堆上。而分代假设主要是将GC目标放在新生变量上,这对go来说性能提升不是很大。
  • 三色标记法
  1. 白色对象:未被回收器访问到的对象。回收开始前所有对象都是白色的,扫描结束后,白色对象为不可达。
  2. 灰色对象:已被回收器访问到的对象,但回收器还需要对其中一个或多个指针对象进行扫描。
  3. 黑色对象:已被回收器访问到的对象,且其中所有指针对象都被扫描或不存在指针对象。
  • 如何保证垃圾回收的正确性
  1. 一个白色对象被黑色对象引用 (白色被挂在黑色下)
  2. 灰色对象与它之间的可达关系的白色对象遭到破坏 (灰色同时丢了该白色) 以上任两个条件同时满足就会破坏垃圾回收的正确行。
  • 不变性
  1. 强三色不变性。黑色对象不对指向白色对象。(插入写屏障)
  2. 弱三色不变性。黑色对象可以指向白色对象,当且仅当存在从灰色对象出发可以找到该白色对象的路径。(删除写屏障)
  • 赋值器的颜色
  1. 黑色赋值器:回收器扫描过,不会再对其进行扫描。
  2. 灰色赋值器:回收器没扫描或者扫描过但还需重新扫描。
  • 在强三色不变性中,黑色赋值器只存在到黑色对象或灰色对象的指针,因为此时所有黑色对象指向白色对象是禁止的。

  • 在弱三色不变性中,黑色赋值器允许存在到白色对象的指针,但这个白色对象应处于保护状态下。

  • 写屏障(由于go协程操作都在栈上,所以栈使用写屏障hook效率太低,写屏障只使用堆内存)

  1. 插入写屏障(dijkstra write barrier):

96fc52c89e9df499c8a8cba7731bcd6.png

  1. 删除写屏障(yuasa)

989031b7388c80e1566b04633da6226.png

  1. 混合写屏障(hybrid)

20cf42438b59b0dde59a78dae42c008.png 继承了插入写屏障的优点,

  • GC流程
  1. Sweep Termination: 对未清扫的span进行清扫, 只有上一轮的GC的清扫工作完成才可以开始新一轮的GC(新GC会协助清理上一个GC的sweep)
  2. Mark: (allcoBits,gcmarkBits)
  • Mark Prepare:初始化GC任务,包括开启写屏障(write barrier)和辅助GC(mutator assist),统计root对象的任务数量等,这个过程需要STW。
  • GC Drains: 扫描所有root对象,包括全局指针和goroutine(G)栈上的指针(扫描对应G栈时需停止该G),将其加入标记队列(灰色队列),并循环处理灰色队列的对象,直到灰色队列为空。该过程后台并行执行。
  1. Mark Termination: 完成标记工作, 重新扫描部分根对象(要求STW)
  2. Sweep: 按标记结果清扫span(一个work后台线程,一个是即时清理:mcache向mcentral申请时再做清除工作。)

目前整个GC流程会进行两次STW(Stop The World), 第一次是Mark阶段的开始, 第二次是Mark Termination阶段.

  • 第一次STW会准备根对象的扫描, 启动写屏障(Write Barrier)和辅助GC(mutator assist).
  • 第二次STW会重新扫描部分根对象, 禁用写屏障(Write Barrier)和辅助GC(mutator assist).

需要注意的是, 不是所有根对象的扫描都需要STW, 例如扫描栈上的对象只需要停止拥有该栈的G.从go 1.9开始, 写屏障的实现使用了Hybrid Write Barrier, 大幅减少了第二次STW的时间。

  • 优化技巧
  1. 尽量避免逃逸。
  2. 使用sync.Pool,重用内存。

- 引用计数

  • 优点
  1. 内存管理的操作均摊到程序执行中。
  2. 内存管理不需要了解runtime的实现细节。
  • 确定
  1. 维护引用计数,需要原子操作。
  2. 无法回收环形结构。weak reference。
  3. 额外的内存开销,存储引用计数。
  4. 大量内存需要回收时可能引起程序暂停。

- go的内存分配

de7ec142698328755ead0f7a9eb2770.png

采用的是类似TCMalloc的分配算法。

  • Page:和tcmalloc的page一样为8KB。
  • Span:代码中为mspan,由一个或多个page组成,内存管理的基本单位。
  • mcache:类似tcmalloc的线程缓存,但是go中是一个P对应一个mcache。mcache保存了各种大小不同的span,并按span class划分,小对象<=32KB直接在mcache分配,起到了缓存作用,并且是无锁分配。
  • mcentral:与tcmalloc的CentralCache一样,是全局缓存,需要加锁访问。同样是按span class分类,多个span串成链表。一个mcentral对应一个span class,有两个链表,分别是nonempty和empty。nonempty链表中的span都至少有一个空闲的对象空间。而empty链表中的span没有空闲的对象空间,已经被mcache取走但还未归还的span。
  • mheap:和tcmalloc的pageheap类似,堆内存的抽象,把从os申请的page组成span并保存起来。mheap把span组织成二叉排序树而不是链表,分为free(空闲并非垃圾回收过的)和scav(被垃圾回收过的),并用heapArena进行管理。

编译优化

  • 函数内联