Go的内存管理

2018-03-05  本文已影响84人  绝望的祖父

本文翻译自Memory Management in Go,介绍了Go语言中内存管理的相关概念。

所有的计算机程序语言都必须处理内存管理,本文将讨论Go语言使用的一些内存管理概念。

堆栈,堆和数据段

在计算机环境中,有三个地方可以分配内存:堆栈,堆和数据段。

堆栈

一部分内存分配是在堆栈上完成的。堆栈拥有一个可以上下移动的顶部,通过向上移动栈顶可以在堆栈上分配空间,并且可以通过将栈顶向下移动来释放空间。栈顶是一个内存地址,可以通过快速的算术运算进行递增或递减。

通常,函数参数和局部变量在堆栈上进行分配。

每一个 goroutine 都拥有自己的堆栈,因此不需要同步(例如加锁)。

Goroutine 的堆栈在堆上进行分配。如果堆栈需要的内存大小超出分配给它的大小,那么将会发送堆操作(分配新的空间,将旧空间的内容复制到新空间中,释放旧空间)

还有一部分内存空间被分配在堆上。与堆栈不同的是,堆并没有一个单个的区域用来分配或者释放。相反,堆中包含一组空闲的区域,必须使用某种数据结构来管理这组空闲区域(一般来说是一棵二叉树)。当在堆上进行内存分配时,将会从这片空闲区域中删除某个结点,当这块内存需要释放时,又会将它添加回这组空闲区域中。

与堆栈不同,堆不属于某个 goroutine, 因此操作堆中空闲区域的集合时需要进行同步(例如锁定)。

数据段

内存也可以在数据段中分配。这是全局变量的存储位置。数据段在程序编译时确定,因此在运行时不会增长或缩小。

将在何处分配内存

Go程序语言规范并未定义将会在何处分配内存。例如,定义为var x int的变量可以分配在堆栈或堆上,并且仍然遵循语言规范。同样,由p := new(int)定义的整形指针也可以分配在堆栈或堆上。

但是,某些既定要求会在某些条件下排除一些选项,例如:

逃逸分析

逃逸分析用来确定内存项是否可以在堆栈上进行分配。它确定一个函数中创建的项目(例如局部变量)是否可能逃逸出该函数,或者到其他 goroutine 中。例如,在下面的函数中,x 从定义它的函数中逃逸出来:

package escapeanalysis
 
 func Foo() *int {
     var x int
     return &x
 }

可能逃逸的项必须分配在堆上,因此 x 将被分配在堆上。

确切的逃逸分析算法可能在不同版本的Go中发生改变。但是,你可以使用go tool compile -m来打印优化策略,其中包含逃逸分析。例如,前面的程序在Go 1.8.3 版本中会得到如下输出:

escape.go:3: can inline Foo
escape.go:5: &x escapes to heap
escape.go:4: moved to heap: x

垃圾回收

Go的内存管理支持垃圾收集。Go的垃圾收集器为了完成任务,偶尔不得不STW(stop the world)。自从1.5版本以来,收集器的设计使得在50毫秒的执行时间内STW任务的时间不会超过10毫秒。

垃圾收集器必须知道堆和堆栈中分配的项目。很容易看出,如果你发现一个堆中分配的项目H,引用了一个堆栈中分配的项目S,很明显,垃圾收集器将不会释放H,直到S被释放掉,因此,垃圾收集器必须知道S的生命周期。

性能

如果你的程序是CPU密集型,请使用 runtime/pprofgo tool pprof来分析程序。如果你发现像 growslice 或 newobject 这样的符号占用了大量时间,那么优化内存分配可能会提高性能。

假设你已经确定优化内存分配可以提高程序性能,那么请减少分配的数量 - 尤其是堆分配。

  1. 重用已分配的内存
  2. 重构你的代码,以便编译器可以进行堆栈分配而不是堆分配。使用go tool compile -m来帮助你识别将被分配到堆上的逃逸变量,然后重写你的代码以便可以进行堆栈分配。
  3. 重构你的CPU密集型代码,以便可以在几个大内存块中进行内存分配,而不是连续分配小块的内存。
上一篇 下一篇

猜你喜欢

热点阅读