Go自动内存管理

179 阅读4分钟

动态内存

内存管理管理的是动态内存,比如C语言中用malloc分配的内存。

自动内存管理(垃圾回收) :

  • 由程序语言的运行时系统管理动态内存,避免手动内存管理,专注于实现业务逻辑。
  • 保证内存使用的正确性和安全性: double-free problem(重复释放), use-after-free problem(释放后使用)

三个任务

  • 为新对象分配空间
  • 找到存活对象
  • 回收死亡对象的内存空间

相关概念

Mutator: 业务线程,分配新对象,修改对象指向关系
Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
Serial GC: 只有一个 collector
Parallel GC: 支持多个 collectors 同时回收的 GC 算法
Concurrent GC: mutator(s) 和 collector(s) 可以同时执行

image.png

如图所示,当Mutators需要回收内存时,Serial GC必须暂停所有的Mutator,用一个Collector去回收内存;Parallel GC支持多个Collector同时回收,性能比Serial GC高一些;当Concurrent GC需要回收内存时,不需要显式地暂停程序,可以把需要进行垃圾回收的Mutator暂停,串行执行Collactor,其他Mutator继续执行。

Concurrent GC的挑战

image.png

由于程序不停止运行,在标记过程中可能有新的对象加入,这个对象也应该标记为存活对象。所以Collectors必须感知对象指向关系的改变。

GC算法的评价

安全性 (Safety):不能回收存活的对象(基本要求)
吞吐率(Throughput):1-GC 时间/程序执行总时间(花在 GC 上的时间越短越好,即吞吐率越高越好)
暂停时间 (Pause time): stop the world (STW)(业务是否感知)
内存开销 (Space overhead):GC 元数据开销

常见垃圾回收算法

追踪垃圾回收(Tracing garbage collection)
引用计数(Reference counting)

追踪垃圾回收

追踪垃圾回收的3个步骤

首先需要明确:当指针指向关系不可达的对象时,该对象应该被回收
回收可以分为3个步骤:

  1. 标记根对象,即静态变量、全局变量、常量、线程栈等。这些变量作用于全局,在整个程序运行过程中都可能用到。
  2. 标记: 找到可达对象。从根对象出发,找到所有可达对象。
  3. 清理: 所有不可达对象。策略有:
    • 将存活对象复制到另外的内存空间(Copying GC)
    • 将死亡对象的内存标记为“可分配”(Mark-sweep GC),用链表保存可分配内存
    • 移动并整理存活对象(Mark-compact GC),即内存空间压缩

根据对象的生命周期,使用不同的标记和清理策略。

分代GC(Generational GC)

分代假说 (Generational hypothesis): most objects die young,大部分对象很快就死了。
根据对象经历的GC次数,我们可以将其像划分年龄一样分为Young Generation和old Generation。针对年轻和老年的对象,制定不同的 GC 策略,降低整体内存管理的开销。
可以认为:年轻代存活数目少,可以用Copying GC处理,开销更少,吞吐率高;而年老代更可能存活。所以用Mark-sweep GC,避免反复复制。碎片多时进行压缩。

引用计数

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

image.png

如图所示,左上角绿圈内的对象,只有一个对象在引用它,所以计数为1;左下角红圈内的对象已经没有其他对象对其引用,计数为0,所以它和被它引用的对象都应被回收。
引用计数的优点是内存管理的操作被平摊到程序执行过程中,内存管理不需要了解 runtime 的实现细节,但是它也有缺点。

引用计数的缺点

  1. 维护引用计数的开销较大: 因为可能会有多个线程操纵同一个对象,所以需要用原子操作保证对引用计数操作的原子性和可见性
  2. 无法回收环形数据结构。当一个环上的所有对象都没被环外对象引用时,这个环中的对象应该被回收。但是它们之间却互相引用,计数不为0。
  3. 内存开销: 每个对象都引入的额外内存空间存储引用数目
  4. 回收内存时依然可能引发暂停