一、基础概念
垃圾回收 GC
自动回收内存的另一个术语是垃圾回收。从广义上讲,
垃圾回收器(garbage collection,简称 GC)是一个系统,它通过识别内存中哪些部分不再需要,来代表应用程序回收内存。
对象与指针
- 对象是一块动态分配的内存,其中包含一个或多个 Go 值。
- 指针是引用对象内任何值的内存地址(这自然包括
*T形式的 Go 值,也包括内置 Go 值的部分。字符串、切片、通道、映射和接口值都包含垃圾回收器(GC)必须追踪的内存地址)。
对象图与根
- 对象及指向其他对象的指针构成对象图。
- 根节点是指那些标识着程序肯定在使用的对象的指针。根节点的两个例子是局部变量和全局变量。
无法肯定被使用的是指那些无法确定生命周期的对象
Go 值存储在哪里
栈分配
例如,存储在局部变量中的非指针 Go 值很可能根本不会由 Go 的垃圾回收器(GC)管理,相反,Go 会安排分配与创建该值时的词法作用域相关联的内存。一般来说,这比依赖垃圾回收器更高效,因为 Go 编译器能够预先确定该内存何时可以被释放,并生成相应的机器指令来进行清理。通常,我们将以这种方式为 Go 值分配内存称为“栈分配”,因为这些空间是存储在 goroutine 栈上的。
为什么就能预先确定何时可以被释放?在编译阶段,Go 编译器会通过静态分析来追踪每一个变量的生命周期。它会问一个关键问题:“这个变量在函数返回后,还会被外部引用吗?”
如果变量只在函数内部使用(例如局部变量,且没有把它的指针传给函数外部),编译器就确定它的生命周期在函数结束时也随之结束。
堆逃逸
那些由于编译器无法确定其生命周期而不能以这种方式分配内存的值,被称为“逃逸到堆上”。
“堆”可以看作是内存分配的一个万能容器,当 Go 语言的值需要被放置在某个地方时,堆就派上用场了。在堆上分配内存的行为通常被称为 “动态内存分配”,因为编译器和运行时对于这些内存的使用方式以及何时可以被清理,几乎无法做出任何假设。这就是垃圾回收器(GC)的用武之地:它是一个专门用于识别并清理动态内存分配的系统。
堆逃逸的传递性如果一个 Go 值的引用被写入另一个已确定会逃逸的 Go 值中,那么该值也必须逃逸。
有关如何确定哪些值会发生逃逸、哪些不会的更多详细信息,请参见消除堆分配部分
GC 工作机制
追踪垃圾回收
垃圾回收器会从程序的根节点开始遍历对象图,根节点是指那些标识着程序肯定在使用的对象的指针。根节点的两个例子是局部变量和全局变量。遍历对象图的过程被称为扫描。在 Go 语言文档中,你可能还会看到另一个表述,即某个对象是否“可访问”,这仅仅意味着该对象可以通过扫描过程被发现。另外需要注意的是,除了一个例外情况,一旦内存变得不可访问,它就会一直保持不可访问的状态。
Go 的垃圾回收器采用标记-清除技术,这意味着为了跟踪其进度,垃圾回收器还会将遇到的值标记为活跃状态。追踪完成后,垃圾回收器会遍历堆中的所有内存,并使所有未被标记的内存可供分配。这个过程称为清除。
标记阶段:GC 从根开始遍历对象图,将遇到的活跃值标记为 “存活”。 清除阶段:追踪完成后,GC 遍历堆中所有内存,释放未标记的内存供重新分配。 非移动 GC:与移动 GC(将对象移至新内存区域并留下转发指针)不同,Go GC 为非移动 GC,不移动对象。
GC 周期
Go 语言的垃圾回收器(GC)是一种标记-清除式垃圾回收器,它大致分为两个阶段:标记阶段和清除阶段。
在所有内存都被追踪完毕之前,无法释放内存以供重新分配,因为可能仍有未扫描的指针使某个对象保持存活状态。
清除操作必须与标记操作完全分离。
垃圾回收器会在清除、关闭和标记这三个阶段不断循环,这就是所谓的垃圾回收周期。就本文档而言,可以认为垃圾回收周期始于清除阶段,接着是关闭阶段,然后是标记阶段。
理解成本
https://tip.golang.org/doc/gc-guide#Understanding_costs
GC 核心参数配置
GOGC 参数
https://tip.golang.org/doc/gc-guide#GOGC
内存限制
https://tip.golang.org/doc/gc-guide#Memory_limit
GC 延迟与高级特性
延迟
https://tip.golang.org/doc/gc-guide#Latency
终结器、清理程序和弱指针
https://tip.golang.org/doc/gc-guide#Finalizers_cleanups_and_weak_pointers