Go 内存优化与垃圾收集

Go提供了自动化的内存管理机制,但在某些情况下需要更精细的微调从而避免发生OOM错误 。本文将讨论Go的垃圾收集器、应用程序内存优化以及如何防止OOM(Out-Of-Memory)错误 。

Go 内存优化与垃圾收集

文章插图
Go中的堆(Heap)栈(Stack)我不会详细介绍垃圾收集器如何工作,已经有很多关于这个主题的文章和官方文档(比如A Guide to the Go Garbage Collector[2]和源码[3]) 。但是,我会提到一些有助于理解本文主题的基本概念 。
你可能已经知道,Go的数据可以存储在两个主要的内存存储中: 栈(stack)和堆(heap) 。
Go 内存优化与垃圾收集

文章插图
通常,栈存储的数据的大小和使用时间可以由Go编译器预测 , 包括函数局部变量、函数参数、返回值等 。
栈是自动管理的,遵循后进先出(LIFO)原则 。当调用函数时,所有相关数据都放在栈的顶部,函数结束时,这些数据将从栈中删除 。栈不需要复杂的垃圾收集机制,其内存管理开销最小 , 在栈中检索和存储数据的过程非常快 。
然而,并不是所有数据都可以存储在栈中 。在执行过程中动态更改的数据或需要在函数范围之外访问的数据不能放在栈上 , 因为编译器无法预测其使用情况,这种数据应该存储在堆中 。
与栈不同,从堆中检索数据并对其进行管理的成本更高 。
栈里放什么,堆里放什么?正如前面提到的,栈用于具有可预测大小和寿命的值,例如:
  • 在函数内部声明的局部变量,例如基本数据类型变量(例如数字和布尔值) 。
  • 函数参数 。
  • 函数返回后不再被引用的返回值 。
Go编译器在决定将数据放在栈中还是堆中时会考虑各种细微差别 。
例如 , 预分配大小为64 KB的数据将存储在栈中,而大于64 KB的数据将存储在堆中 。这同样适用于数组,如果数组超过10 MB,将存储在堆中 。
可以使用逃逸分析(escape analysis)来确定特定变量的存储位置 。
例如,可以通过命令行编译参数-gcflags=-m来分析应用程序:
go build -gcflags=-m mAIn.go如果使用-gcflags=-m参数编译下面的main.go:
package mainfunc main() {var arrayBefore10Mb [1310720]intarrayBefore10Mb[0] = 1var arrayAfter10Mb [1310721]intarrayAfter10Mb[0] = 1sliceBefore64 := make([]int, 8192)sliceOver64 := make([]int, 8193)sliceOver64[0] = sliceBefore64[0]}结果是:
# command-line-arguments./main.go:3:6: can inline main./main.go:7:6: moved to heap: arrayAfter10Mb./main.go:10:23: make([]int, 8192) does not escape./main.go:11:21: make([]int, 8193) escapes to heap可以看到arrayAfter10Mb数组被移动到堆中 , 因为大小超过了10MB,而arrayBefore10Mb仍然留在栈中(对于int变量,10MB等于10 * 1024 * 1024 / 8 = 1310720个元素) 。
此外,sliceBefore64没有存储在堆中 , 因为它的大小小于64KB,而sliceOver64被存储在堆中(对于int变量,64KB等于64 * 1024 / 8 = 8192个元素) 。
要了解更多关于在堆中分配的位置和内容,可以参考malloc.go源码[4] 。
因此,使用堆的一种方法是尽量避免用它!但是,如果数据已经落在堆中了呢?
与栈不同,堆的大小是无限的,并且不断增长 。堆存储动态创建的对象,如结构体、分片和映射,以及由于其限制而无法放入栈中的大内存块 。
在堆中重用内存并防止其完全阻塞的唯一工具是垃圾收集器 。
浅谈垃圾收集器的工作原理垃圾收集器(GC)是一种专门用于识别和释放动态分配内存的系统 。
Go使用基于跟踪和标记和扫描算法的垃圾收集算法 。在标记阶段,垃圾收集器将应用程序正在使用的数据标记为活跃堆 。然后,在清理阶段,GC遍历所有未标记为活跃的内存并复用 。
垃圾收集器不是免费工作的 , 需要消耗两个重要的系统资源: CPU时间和物理内存 。
垃圾收集器中的内存由以下部分组成:
  • 活跃堆内存(在前一个垃圾收集周期中标记为"活跃"的内存)
  • 新的堆内存(尚未被垃圾收集器分析的堆内存)
  • 存储元数据的内存,与前两个实体相比,这些元数据通常微不足道 。
垃圾收集器所消耗的CPU时间与其工作细节有关 。有一种称为"stop-the-world"的垃圾收集器实现,它在垃圾收集期间完全停止程序执行,导致CPU时间被花在非生产性工作上 。


推荐阅读