这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天
简介
Go 语言负责安排 Go 值的存储;在大多数情况下,Go 开发人员不需要关心这些值存储在哪里。然而在实践中,这些值通常需要存储在计算机物理内存中,而物理内存是一种有限的资源。因为它是有限的,所以必须仔细管理和回收内存,以避免在执行 Go 程序时耗尽它。根据需要分配和回收内存是 Go 实现的工作。
自动回收内存的另一个术语是垃圾收集。在高层次上,垃圾收集器(或简称 GC)是一个系统,它通过识别不再需要内存的哪些部分来代表应用程序回收内存。Go 标准工具链提供了每个应用程序都附带的运行时库,这个运行时库包括一个垃圾收集器。
请注意,Go 规范不保证本指南中描述的垃圾收集器的存在,只是 Go 值的底层存储由语言本身管理。这种遗漏是有意的,可以使用完全不同的内存管理技术。
因此,关于 Go 编程语言的特定实现,可能不适用于其他实现。具体来说,这篇笔记适用于标准工具链(gc Go 编译器和工具)。 Gccgo 和 Gollvm 都使用非常相似的 GC 实现,因此适用许多相同的概念,但细节可能有所不同。
Go的值存储位置
在我们深入探讨GC之前,我们先讨论一下不需要由GC管理的内存。
例如,存储在局部变量中的非指针式Go值很可能根本不受Go GC的管理,Go会安排分配与创建该变量的词法范围相关的内存。一般来说,这比依赖GC更有效,因为Go编译器能够预先确定该内存何时被释放,并发出机器指令进行清理。通常情况下,我们把以这种方式为Go值分配内存称为 "堆栈分配",因为空间是存储在goroutine堆栈中的。
因为Go编译器无法确定其寿命,所以不能以这种方式分配内存的Go值被称为“逃逸到堆中”。"堆"可以被认为是内存分配的集合体,用于Go值需要放置的地方。在堆上分配内存的行为通常被称为 "动态内存分配",因为编译器和运行时对这些内存的使用情况以及何时清理都不能做出任何假设。这就是GC的作用:它是一个专门识别和清理动态内存分配的系统。
有许多原因导致Go值可能需要转移到堆中。其中一个原因可能是它的大小是动态确定的。例如,考虑一个分片的支持数组,其初始大小是由一个变量而不是常数决定的。请注意,逃逸到堆中也必须是超越性的:如果对一个Go值的引用被写入另一个已经被确定为逃逸的Go值中,该值也必须逃逸。
一个围棋值是否转义是由它的使用环境和围棋编译器的转义分析算法决定的。试图精确地列举值的转义是很脆弱和困难的:算法本身是相当复杂的,而且在不同的Go版本中会有变化。
跟踪垃圾回收
垃圾收集可能指的是许多不同的自动回收内存的方法;例如,引用计数。具体在有些上下文中,垃圾收集指的是跟踪垃圾收集,它通过跟踪指针传递地识别正在使用的、所谓的活动对象。更准确的定义如下:
- Object—对象是一块动态分配的内存,其中包含一个或多个 Go 值。
- Pointer—引用对象内任何值的内存地址。这自然包括 *T 形式的 Go 值,但也包括部分内置的 Go 值。字符串、切片、通道、映射和接口值都包含 GC 必须跟踪的内存地址。
对象和指向其他对象的指针一起构成了对象图。为了识别活动内存,GC 从程序的根开始遍历对象图,指针标识程序明确使用的对象。根的两个例子是局部变量和全局变量。遍历对象图的过程称为扫描。
这个基本算法对所有跟踪 GC 都是通用的。跟踪 GC 的不同之处在于它们发现内存处于活动状态后所做的事情。 Go 的 GC 使用 mark-sweep 技术,这意味着为了跟踪它的进度,GC 也会将它遇到的值标记为 live。跟踪完成后,GC 将遍历堆中的所有内存,并使所有未标记为可供分配的内存。这个过程称为清扫。
一种替代技术是将对象实际移动到内存的新部分并留下一个转发指针,稍后用于更新所有应用程序的指针。我们将以这种方式移动对象的 GC 称为移动 GC; Go 有一个不动的 GC。