Go 语言内存管理详解 | 青训营笔记

109 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

内存优化是Go语言开发不可或缺的环节,而Go语言内存管理就是优化环节的重要组成部分。在这一章中会对Go语言SDK及其编译器内存管理优化进行详细总结。

专业名词扫盲

优化

业务层优化:针对特定场景,具体问题。
语言运行优化:解决更通用的性能问题。

性能优化注意事项:稳定,测试用例,优化内容文档,通过选项控制是否优化,必要的日志输出。

内存管理优化

  1. 自动内存管理

相关概念:
Grabage collction: 垃圾回收
Mutator: 业务线程,分配新对象,修改对象指向关系
Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
Serial GC: 只有一个 collector,此时业务线程暂停
Parallel GC: 支持多个 collectors 同时回收的 GC 算法,此时业务线程暂停
Concurrent GC: mutator(s) 和 collector(s) 可以同时执行

GC评价指标:安全,吞吐率,暂停时间,内存开销。

  1. 追踪垃圾回收
    标记根对象,包括全局变量global、堆heap、栈stack。
    找到可达对象。
    清理不可达对象,包括三种方式:
    1、将存活对象复制到另外的内存空间(Copying GC)。
    2、将死亡对象的内存标记为"可分配”(Mark-sweep GC)。
    3、移动并整理存活对象(Mark-compact GC)。

  2. 分代GC
    根据生命周期选择不同的回收机制。
    基于分代假说:很多对象分配出来之后很快就不用了。
    不同年龄(经过GC的次数)对象分配在不同heap区域。
    对于年轻代,用Copying GC处理。对于老年代,用Mark-compact GC处理。

  3. 引用计数
    每个对象都有一个与之关联的引用数目,当且仅当引用数大于0时对象存活。
    优点:程序进行过程中维护对象的计数,甚至不需要了解runtime的实现细节。
    缺点:维护开销比较大,无法回收环形数据结构,需要额外内存,回收内存时需要暂停。

Go内存分配

  1. 为对象在heap上分配内存:
    mmap()向OS申请大块内存,分割成次大块内存mspan,继续分割成特定大小的小块,分配不包含指针的对象称为noscan mspan(GC不扫描),分配包含指针的对象scan mspan(GC需要扫描)。
    缓存mcache管理一组mspan,当管理的内存分配完了,就向mcentral申请还有没分配块的mspan,对应的,如果mspan中还有没分配块,会缓存在mcentral中,而不是立刻释放归还给OS。

  2. Go内存分配流程比较长,需要进行优化。

  3. 优化方案Balanced GC
    每个g分配1KB内存,成为GAB,用于noscan类型对象分配,用三个指针进行维护,需要内存分配时,直接移动指针分配出一块内存。
    缺点:导致内存延迟释放。
    解决方案:当GAB大小超过一定阈值时,把存活的对象移动到一个新创建的GAB中,类似copying GC。

编译器优化

  1. 编译器结构

编译器结构.png

  1. 静态分析:不执行代码,推到程序行为
    控制流(Control flow): 程序执行的流程。
    数据流(Data flow): 数据在控制流上的传递。

  2. 过程内分析和过程间分析

Go编译器优化

  1. 函数内联(InLinning)
    将被调用函数的函数体(callee) 的副本替换到调用位置(caller) 上,同时重写代码以反映参数的绑定。
    优点:消除函数调用开销,将过程间分析转化为过程内分析。
    缺点:函数体变大,编译生成的Go镜像变大。

  2. Beast Mode
    调整函数内联的策略,使更多函数被内联。
    降低函数调用开销,增加其他优化机会。

  3. 逃逸分析
    分析代码中指针的动态作用域:指针在何处可以被访问。
    大致思路:

从对象分配处出发,沿着控制流,观察对象的数据流
若发现指针 p 在当前作用域 s:
	作为参数传递给其他函数
	传递给全局变量
	传递给其他的 goroutine
	传递给已逃逸的指针指向的对象
则指针 p 指向的对象逃逸出 s,反之则没有逃逸出 s