Go语言进阶2 | 青训营笔记

84 阅读4分钟

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

自动内存管理(GC)

Mutator:业务线程,分配新对象,修改对象指向关系
Collector:GC线程,找到存活对象,回收死亡对象的内存空间

GC算法:

  • Serial GC:只有一个 Collector
  • Parallel GC:支持多个 Colletors同时回收
  • Concurrent GC:Collector(s) 和 Mutator(s)可以同时执行
    Collector(s) 必须感知到对象指向关系的改变 (已标记为存活的对象所指向的对象必须要标记)

  • 追踪垃圾回收 Tracing Garbage Collection

对象被回收的条件:指针指向关系不可达的对象
标记根对象:静态变量、全局变量、常量、线程栈等
标记:找到所有可达对象(求指针指向关系的传递闭包
清理所有不可达对象:清理策略
Copying GC:将存活对象复制到另外的内存空间
Mark-sweep GC:将死亡对象的内存标记为“可分配”(使用freelist管理空闲内存)
Mark-compact GC:移动并整理存活对象(原地整理对象)
根据对象的生命周期,使用不同的标记和清理策略


  • 分代GC   Generational GC

基于分代假说(Generational hypothesis):most objects die young.
每个对象都有“年龄”:经历过 GC 的次数
目的:对年老和年轻的对象制定不同的GC策略,降低整体内存管理的开销
不同年龄的对象处在heap的不同区域

年轻代:由于存活的对象少,可以使用 Copying GC;GC 吞吐率高
年老代:趋于一直活着,反复复制开销大,可以使用 Mark-sweep GC


  • 引用计数 Reference Counting

每个对象都有一个与之关联的引用数目
对象存活:当且仅当引用数大于0

优点:内存管理的操作被平摊到程序运行中 & 不需要了解 runtime 的实现细节
缺点:

  • 维护开销大,通过原子操作保证对引用计数操作的原子性和可见性
  • 无法回收环形引用
  • 内存开销:每个对象都引入额外内存空间
  • 回收(大数据结构)内存时依然可能引发暂停


Go内存分配

Go 内存分配 ———— 提前将内存分块 mspan 再继续划分为小块
noscan mspan:分配不包含指针的对象 ———— GC不需要扫描
scan mspan:分配包含指针的对象 ———— GC需要扫描

Go 内存分配 ———— 对内存做多级缓存
借鉴 TCMalloc(Thread Caching)
mcache 管理一组 mspan,每个p包含一个 mcache用于快速分配,为绑定与p上的g分配对象
当 mspan 中没有分配对象(mspan为空)时,不会直接释放归还给 OS,而是会被缓存到 mcentral 中供其他 mcache 使用

分配路径长
优化方案:Balanced GC
每个 g 绑定一大块内存(1KB),称作 goroutine allocation buffer(GAB)
GAB 用于 nospan 的小对象(< 128B)分配
使用三个指针 start,end,top 维护GAB
使用 Bump Pointer(指针碰撞)风格作对象分配,只需移动指针,且不需与其他 g 互斥
GAB 对 go内存管理来说是一个大对象(把多个小对象的分配合并成一个大对象的分配
问题:只有一个小对象时,也导致 GAB 占用的内存延迟释放
方案:移动存活的对象(用 Copying GC 管理小对象);当 g 的 GAB 总大小超过一定阈值时,将 GAB 中存活的对象复制到另外分配的 GAB 中,原先的 GAB 可以释放



编译器和静态分析(编译优化)

静态分析:不执行代码,推导程序的行为,分析程序的性质
控制流:程序执行的流程(代码块组成控制流图)
数据流:数据在控制流上的传递

过程内分析:仅在函数内部进行分析
过程间分析:考虑过程调用时参数传递和返回值的数据流和控制流

Go 编译器优化

函数内联(Inlining):消除函数调用开销;把过程间分析转化为过程内分析

逃逸分析:分析代码中指针的动态作用域(指针在何处被访问)

  • 从对象分配处出发,沿着控制流,观察对象的数据流
  • 是否 作为参数传递给其他函数;传递给全局变量;传递给其他goroutine;传递给已逃逸的指针所指的对象

未逃逸的对象可以在栈上分配,分配与回收的速度更快,并减少了在 heap 上的分配,降低了 GC 负担