go在调用c函数,比如调用著名的malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程序员。一不小心,就会发生内存泄漏。Golang 通过逃逸分析,对内存管理进行的优化和简化,它可以决定一个变量是分配到堆上还是栈上,便可以处理内存泄露问题。
Golang 的逃逸分析,是指编译器根据代码的特征和生命周期,自动把变量分配到堆或者是栈上面。
golang程序变量栈上逃逸堆上分配
这里简单介绍一下堆和栈:
栈( stack)是系统自动分配空间的,例如我们定义一个 char a;系统会自动在栈上为其开辟空间。
堆(heap)则是程序员根据需要自己申请的空间,例如 malloc(10);开辟十个字节的空间。
栈在内存中是从高地址向下分配的,并且连续的,遵循先进后出原则。系统在分配的时候已经确定好了栈的大小空间。栈上面的空间是自动回收的,所以栈上面的数据的生命周期在函数结束后,就被释放掉了。
堆分配是从低地址向高地址分配的,每次分配的内存大小可能不一致,导致了空间是不连续的,这也产生内存碎片的原因。由于是程序分配,所以效率相对慢些。而堆上的数据只要程序不释放空间,就一直可以访问到,不过缺点是一旦忘记释放会造成内存泄露。
能引起变量逃逸到堆上的典型情况:
- 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
- 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
- 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
- slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
- 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。