这是我参与「第五届青训营 」笔记创作活动的第6天
GC的作用
- 避免手动管理内存
- 保证程序执行时内存的安全性和正确性
GC的三个任务
- 分配新对象的内存空间
- 找到存活的对象
- 释放死亡对象的内存空间
GC相关名词概念
-
Mutator:业务线程,用于创建新对象,并修改对象的引用关系
-
Collector:GC线程,用于回收死亡对象的内存和找到存活的对象
-
Serial GC:只有一个Collector线程
-
Parallel GC:有多个Colleator线程执行GC操作
-
Concurrent GC:mutator(s)和collector(s)可以同时执行
Concurrent GC必须要感知对象指向关系的改变,不然GC可能会出错,例如下图:
在GC过程中mutator业务线程创建了对象b,这个对象被o指向,如果不给b对象标记存活,那么b会在gc过程中被回收,出现gc错误!
评价GC算法的几个标准
- 安全性:不能回收存活的对象
- 吞吐率:1-gc使用时间/程序执行总时间(愈高愈好)
- 暂停时间:stop the world是否影响业务
- 内存开销
追踪垃圾回收
-
初始化标记:全局变量、栈、常量、静态变量等
-
标记可达对象:计算传递闭包,找到所有可以到达的对象
-
清理:处理剩下的不可达的对象
- 清理方式
-
Copying GC:开辟一块新的内存空间,将存活的对象复制过去
-
Mark-Compact GC:移动存活的对象到内存空间的开头
-
Mark-sweep GC:使用free-list连接所有空闲内存,将释放的内存连接到free-list上
-
- 清理方式
清理策略(Generational GC,分代GC)
- 分代假说:大部分对象分配出来没多久就死亡
- 对象年龄:经历的GC次数越多年龄越大
- 将堆内存分为年轻区(young generation)和老年区(old generation),年轻区在内存头,老年区在内存尾
- 年轻区采用copying gc
- 老年区采用Mark-sweep gc
引用计数
- 对象被引用,计数加一
- 计数=0清除对象
- 优点
- 将gc放到程序执行中进行
- 内存管理不需要知道runtime的细节
- 缺点
- 环形数据结构无法清除,每一个对象的计数都不为0
- 可能会引起暂停
- 较大的内存开销
- 可能有多个线程引用同个对象,计数器增加减少需要原子操作,维护计数器的开销较大
GO内存分配机制
分块机制
- 先调用mmap系统调用申请一大块内存空间
- 之后调用mspan在内存空间切分成大块
- 在大块中分出小块,对象分配在小块中
- mspan类别
- noscan mspan:分配的对象包含指针,GC不需要扫描
- scan mspan:分配的对象不包含指针,GC需要扫描
例如上图中8KB是mspan分出来的大块,每个大块中的8B、16B是小块
缓存机制
缓存过程图如下:
- GO内存分配借鉴了TCMalloc:Thread Caching Malloc
- 每一个p包含一个mcache用于分配内存,用于为绑定的g分配内存
- mcache管理一组mspan
- 首先在mspans中查找空闲的小块,如果没有空闲mspan,就去mcentral中找空闲的mspan
- mcentral中mspan空闲并不会立即释放并归还OS,而是缓存在mcentral中
优化
GO语言内存分配特点:
- GO语言分配对象路线很长,g->m->p->mcache->mspan->memory block->return pointer
- 对象分配占用的CPU时间比较长
- 小对象分配居多
优化策略:采用Balanced GC
Balanced GC
- 给每一个g单独分配一个GAB(goroutine allocation buffer),大小为1KB
- 在goroutine申请内存的时候只需要在GAB上分配即可
- GAB上有三个指针base、top、end,分配的时候移动top指针并检验是否越界即可
GAB图例:
分配算法:
GAB对于GO内存管理来说就是一个大对象,本质其实是将小对象的分配合并;GAB可能会导致内存的延迟释放,因为GAB内只要有一个对象存活,GAB就不会被释放。
可以采用survivor GAB来存储存活下来的对象,使用copying gc算法管理这些小对象。
编译器优化
编译器编译流程:
IR:Intermediate Representation中间表示
静态分析
不执行程序,分析程序的执行过程,可以得到数据流和控制流
- 控制流:程序执行的流程
- 数据流:数据在控制流上的传递
- 通过数据流,可以得到更多关于程序的性质,可能可以优化程序
过程内分析和过程间分析
- 过程内分析:在函数内部分析代码优化
- 过程间分析:函数内有函数调用,考虑函数调用时的参数传递和返回值的数据流和控制流
- 过程间分析需要同时分析数据流和控制流
GO编译器特点
为了减少编译时间,不会做过多的代码优化;自带函数内联优化,但是条件很严格,大部分场景不会优化函数内联;
函数内联:
- 优点:
- 可以减少运行时间(减少了函数调用)
- 逃逸分析中,逃逸的指针变少
- 缺点:
- GO镜像变大
- GO函数体过长,对instruction cache不友好
优化策略:Beast Mode
Beast Mode
放宽了函数内联的限制条件,让更多函数可以内联,函数体超出限制条件长度之后不内联。内联之后做了新的逃逸分析,对于没有逃逸的指针,将其分配在栈内存中。