这是我参与「第三届青训营 -后端场」笔记创作活动的第四篇笔记。
1 自动内存管理
1.1 相关概念
Mutator:业务线程,分配新对象,修改对象指向关系
Collector: GC线程,找到存活对象,回收死亡对象的内存空间
Serial GC: 只- collector
Parallel GC:支持多个 collectors同时回收的GC算法
Concurrent GC: mutator(s) 和 collector(s) 可以同时执行
1.2 追踪垃圾回收
- 对象被回收的条件:指针指向关系不可达的对象
- 标记根对象
静态变量、全局变量、常量、线程栈等 - 标记:找到可达对象
求指针指向关系的传递闭包:从根对象出发,找到所有可达对象 - 清理:所有不可达对象
将存活对象复制到另外的内存空间(Copying GC)
将死亡对象的内存标记为“可分配" (Mark-sweep GC)
移动并整理存活对象(Mark-compact GC) - 根据对象的生命周期,使用不同的标记和清理策略
1.3 分代GC(Generational GC)
每个对象都有年龄:经历过GC的次数,很多对象在分配出来后很快就不再使用了,针对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销
年轻代存活对象少,可以使用copying collection,老年代趋于一直存活,反复复制开销大,可以采用mark-sweep collection
1.4 引用计数
每个对象都有一个与之关联的引用数目
对象存活的条件:当且仅当引用数大于0
优点
内存管理的操作被平摊到程序执行过程中
内存管理不需要了解runtime的实现细节: C++智能指针(smart pointer)
缺点
维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性
无法回收环形数据结构——weak reference
内存开销:每个对象都引入的额外内存空间存储引用数目
回收内存时依然可能引发暂停(如大型结构)
2 Go内存管理及优化
2.1 内存分配
为对象在heap上分配内存
分块
提前将内存分块,先申请一大块内存,然后把内存分成大块mspan,再将大块分成特定大小的小块,然后分配不包含指针的对象npscan mspan和包含指针的对象scan mspan。
缓存
mcache管理一组mspan,有缓存
2.2 Go内存管理优化
- 对象分配是高频服务,因此优化很关键
- 小对象占比高
- 内存分配比较耗时
2.3 优化方案 Balance GC
每个g都绑定一大块内存(1 KB),称作 goroutine allocation buffer (GAB)
GAB 用于noscan类型的小对象分配:< 128B
使用三个指针维护GAB:base, end, top
Bump pointer (指针碰撞)风格对象分配,无须和其他分配请求互斥,分配动作简单高效
3 编译器和静态分析
3.1 编译器的结构
3.2 静态分析
静态分析:不执行程序代码,推导程序的行为,分析程序的性质。
控制流(Control flow):程序执行的流程
数据流(Data flow):数据在控制流上的传递
3.3 过程内分析和过程间分析
过程内分析(Intra-procedural analysis):仅在函数内部进行分析
过程间分析(Inter-procedural analysis):考虑函数调用时参数传递和返回值的数据流和控制流
4 Go编译器优化
优点:用户无感知且通用
- 函数内联
- 逃逸分析
- 默认栈大小调整
- 边界检查消除
- 循环展开
4.1 函数内联
将被调用函数的函数体(callee)的副本替换到调用位置(caller)上,同时重写代码以反映参数的绑定
优点
消除函数调用开销,例如传递参数、保存寄存器等
将过程间分析转化为过程内分析,帮助其他优化,例如逃逸分析
缺点
函数体变大,instruction cache (icache) 不友好
编译生成的Go镜像变大
大多数情况下函数内联都是正向优化