Go语言内存管理|青训营笔记(五)
这是我参与「第五届青训营 」笔记创作活动的第5天,本文主要介绍了go语言的内存管理机制以及编译器的优化
1.自动内存管理(垃圾回收)
1.1 GC相关概念
自动内存管理又称垃圾回收(Garbage Collector,简称GC)是管理的动态内存,而动态内存是指程序在运行时根据需求动态分配的内存。而自动内存管理则是由程序语言在运行时系统管理动态内存,这样就能避免程序员手动管理内存,更专注于业务设计;也能保证内存的安全性和正确性。
自动内存管理主要有三大任务:
- 为新对象分配内存;
- 找到存活对象;
- 回收死亡对象的内存。
自动内存管理中有以下这些概念需要搞清:
- Mutator:业务线程,分配新对象,修改对象指向关系;
- Collector:GC线程,找到存活对象,回收死亡对象的内存空间;
- Serial GC:只有一个collector,且当GC线程运行时,业务线程会暂停;
- Parallel GC:支持多个collectors同时回收的GC算法,同样会暂停业务线程;
- Concurrent GC:Mutator和collector可同时运行;由于Mutator运行时可能会分配新对象,所以collectors必须可以感知对象指向关系的变化。
评价一个GC算法的性能可以从以下几个方面考察:
- 安全性:不能回收存活对象(基本要求)
- 吞吐率:,最主要用于计算GC在总程序时间中的占比
- 暂停时间:判断业务是否感知
- 内存开销:GC中元数据所占用的内存
1.2 追踪垃圾回收
自动内存管理中最重要的功能就是能及时回收死亡对象内存,而死亡对象可以定义为指针指向关系不可达的对象,这也是对象被回收的条件。在垃圾回收中,一般分为两个步骤:标记、清理;标记是从标记根对象(静态变量、全局变量、常量、线程栈等)出发,找到所有可达的对象,并标记这些对象。清理是将没有被标记的也就是不可达的对象回收内存,根据对象的生命周期,可以使用不同标记、清理方式,主要分为以下三种:
- Copying GC:将存活的对象复制到另外的内存空间,再将当前内存清空。
- Mark-sweep GC:将未标记的死亡的对象的内存标记为“可分配”。
- Mark-compact GC:移动并整理存活对象(原地操作,不占用额外内存)。
1.3 分代GC(Generational GC)
分代假说是指很多对象在分配出来后很快就不再使用了。可以将每个对象经过垃圾回收的次数作为对象的年龄,年龄越小,则表示对象刚被创建出,随时有可能会被回收;而年龄越大则表示对象一直趋于活着,反复复制开销大。可以看到不同年龄有不同的特点,为此可以将内存分为两部分,一部分存储年轻代,一部分存储老年代。对于不同的年龄,可以制定不同GC策略,从而降低整体内存管理开销。对于年轻代,由于存活数较少,可以使用Copying GC;而对于老年代,存活时间较长,可以使用Mark-sweep GC
1.4 引用计数
引用计数是指每个对象都有一个与之关联的引用数目,只有当数目大于零时才表示对象存活;当计数为零时也就表示没有该对象没有被引用,也就是死亡对象。引用计数的优缺点如下:
- 优点
- 内存管理的操作被平摊到程序执行过程中。
- 内存管理不需要了解runtime的实现细节,如C++中的智能指针。
- 缺点
- 维护引用计数的开销较大:通过原子操作保证对引用计数的原子性和可见性。
- 无法回收环形数据结构。
- 内存开销:每个对象都引入额外的内存存储引用数目。
- 回收内存时依然可能引发暂停。
2. Go语言内存管理
2.1 分块
GO语言在分配内存前,会先将内存分块,其具体步骤如下:
- 使用系统调用
mmap()向OS申请一大块内存。 - 将内存划分为大块,称为
mspan。mspan又可分为两类:
noscan mspan:分配不包含指针的对象——GC不需要扫描scan mspan:分配包含指针的对象——GC需要扫描
- 再将大块继续划分为特定大小的小块,用于对象分配。
在给对象分配内存时,只需根据内存的大小选择最合适的块返回。
2.2 缓存
Go语言采用的TCMalloc从程序的每个goroutine出发,每个进程都需要分配内存。每个m会绑定一个p,而每个p包含一个mcache用于快速分配,用于绑定p上的g分配对象。mcahe管理一组mspan,当g需要内存时就通过mcahe