Go语言内存管理|青训营笔记(五)

88 阅读4分钟

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算法的性能可以从以下几个方面考察:

  • 安全性:不能回收存活对象(基本要求
  • 吞吐率:1GC时间程序执行总时间1-\frac{GC时间}{程序执行总时间},最主要用于计算GC在总程序时间中的占比
  • 暂停时间:判断业务是否感知
  • 内存开销:GC中元数据所占用的内存

1.2 追踪垃圾回收

自动内存管理中最重要的功能就是能及时回收死亡对象内存,而死亡对象可以定义为指针指向关系不可达的对象,这也是对象被回收的条件。在垃圾回收中,一般分为两个步骤:标记、清理;标记是从标记根对象(静态变量、全局变量、常量、线程栈等)出发,找到所有可达的对象,并标记这些对象。清理是将没有被标记的也就是不可达的对象回收内存,根据对象的生命周期,可以使用不同标记、清理方式,主要分为以下三种:

  • Copying GC:将存活的对象复制到另外的内存空间,再将当前内存清空。
  • Mark-sweep GC:将未标记的死亡的对象的内存标记为“可分配”。
  • Mark-compact GC:移动并整理存活对象(原地操作,不占用额外内存)。

1.3 分代GC(Generational GC)

分代假说是指很多对象在分配出来后很快就不再使用了。可以将每个对象经过垃圾回收的次数作为对象的年龄,年龄越小,则表示对象刚被创建出,随时有可能会被回收;而年龄越大则表示对象一直趋于活着,反复复制开销大。可以看到不同年龄有不同的特点,为此可以将内存分为两部分,一部分存储年轻代,一部分存储老年代。对于不同的年龄,可以制定不同GC策略,从而降低整体内存管理开销。对于年轻代,由于存活数较少,可以使用Copying GC;而对于老年代,存活时间较长,可以使用Mark-sweep GC image.png

1.4 引用计数

引用计数是指每个对象都有一个与之关联的引用数目,只有当数目大于零时才表示对象存活;当计数为零时也就表示没有该对象没有被引用,也就是死亡对象。引用计数的优缺点如下:

  • 优点
  1. 内存管理的操作被平摊到程序执行过程中。
  2. 内存管理不需要了解runtime的实现细节,如C++中的智能指针。
  • 缺点
  1. 维护引用计数的开销较大:通过原子操作保证对引用计数的原子性和可见性。
  2. 无法回收环形数据结构。
  3. 内存开销:每个对象都引入额外的内存存储引用数目。
  4. 回收内存时依然可能引发暂停。

2. Go语言内存管理

2.1 分块

GO语言在分配内存前,会先将内存分块,其具体步骤如下:

  1. 使用系统调用mmap()向OS申请一大块内存。
  2. 将内存划分为大块,称为mspan。mspan又可分为两类:
  • noscan mspan:分配不包含指针的对象——GC不需要扫描
  • scan mspan:分配包含指针的对象——GC需要扫描
  1. 再将大块继续划分为特定大小的小块,用于对象分配。

在给对象分配内存时,只需根据内存的大小选择最合适的块返回。

2.2 缓存

Go语言采用的TCMalloc从程序的每个goroutine出发,每个进程都需要分配内存。每个m会绑定一个p,而每个p包含一个mcache用于快速分配,用于绑定p上的g分配对象。mcahe管理一组mspan,当g需要内存时就通过mcahe

image.png