一、基础概念
垃圾回收 GC
自动回收内存的另一个术语是垃圾回收。从广义上讲,
垃圾回收器(garbage collection,简称 GC)是一个系统,它通过识别内存中哪些部分不再需要,来代表应用程序回收内存。
对象与指针
- 对象是一块动态分配的内存,其中包含一个或多个 Go 值。
- 指针是引用对象内任何值的内存地址(这自然包括
*T形式的 Go 值,也包括内置 Go 值的部分。字符串、切片、通道、映射和接口值都包含垃圾回收器(GC)必须追踪的内存地址)。
对象图与根
- 对象及指向其他对象的指针构成对象图。
- 根节点是指那些标识着程序肯定在使用的对象的指针。根节点的两个例子是局部变量和全局变量。
无法肯定被使用的是指那些无法确定生命周期的对象
Go 值存储在哪里
栈分配
例如,存储在局部变量中的非指针 Go 值很可能根本不会由 Go 的垃圾回收器(GC)管理,相反,Go 会安排分配与创建该值时的词法作用域相关联的内存。一般来说,这比依赖垃圾回收器更高效,因为 Go 编译器能够预先确定该内存何时可以被释放,并生成相应的机器指令来进行清理。通常,我们将以这种方式为 Go 值分配内存称为“栈分配”,因为这些空间是存储在 goroutine 栈上的。
为什么就能预先确定何时可以被释放?在编译阶段,Go 编译器会通过静态分析来追踪每一个变量的生命周期。它会问一个关键问题:“这个变量在函数返回后,还会被外部引用吗?”
如果变量只在函数内部使用(例如局部变量,且没有把它的指针传给函数外部),编译器就确定它的生命周期在函数结束时也随之结束。
堆逃逸
那些由于编译器无法确定其生命周期而不能以这种方式分配内存的值,被称为“逃逸到堆上”。
“堆”可以看作是内存分配的一个万能容器,当 Go 语言的值需要被放置在某个地方时,堆就派上用场了。在堆上分配内存的行为通常被称为 “动态内存分配”,因为编译器和运行时对于这些内存的使用方式以及何时可以被清理,几乎无法做出任何假设。这就是垃圾回收器(GC)的用武之地:它是一个专门用于识别并清理动态内存分配的系统。
堆逃逸的传递性如果一个 Go 值的引用被写入另一个已确定会逃逸的 Go 值中,那么该值也必须逃逸。
有关如何确定哪些值会发生逃逸、哪些不会的更多详细信息,请参见消除堆分配部分
GC 工作机制
追踪垃圾回收
Go 的垃圾回收器采用标记-清除技术,这意味着为了跟踪其进度,垃圾回收器还会将遇到的值标记为活跃状态。追踪完成后,垃圾回收器会遍历堆中的所有内存,并使所有未被标记的内存可供分配。这个过程称为清除。
非移动 GC与移动 GC(将对象移至新内存区域并留下转发指针)不同,Go GC 为非移动 GC,不移动对象。
GC 周期
Go 语言的垃圾回收过程可以看作是一个不断循环的状态机,被称为 GC 周期。
从宏观上看,GC 在以下三个状态中循环:
- Sweeping(清理):回收上一轮标记出的垃圾。
- Off(空闲):GC 未运行,等待触发。
- Marking(标记):扫描存活对象。
清除操作必须与标记操作完全分离在所有内存都被追踪完毕(标记完成)之前,无法释放内存,因为可能仍有未扫描的指针指向某个对象。
为什么通常认为周期始于“清除阶段”?Go 采用的是并发清除(Concurrent Sweep)。当标记阶段结束时,它不会立即同步清理所有垃圾,而是将清理工作推迟。
因此,当新的 GC 周期被触发时,它的第一件事往往是确保上一个周期留下的清除工作已经彻底完成(即 Sweep Termination),然后再开启新的标记过程。这样可以最大程度减少 STW 停顿时间。
深入了解
如果你对 GC 的具体执行阶段(如 STW、写屏障、协助标记等)感兴趣,可以阅读: