《Go程序员面试笔试宝典》学习笔记
在Go语言中,程序员不需要再担心内存泄漏。虽然Go也有内建函数new,但调用new函数得到的内存不一定在堆上,还有可能在栈上。这是因为在Go语言中,堆和栈的区别被模糊化,这一切都是Go编译器在后台完成的。
一个变量是在栈上分配,还是在堆上分配,是经过编译器的逃逸分析之后得到的结论。堆内存分配导致垃圾回收的开销远远大于栈空间分配与释放的开销。
1.1 逃逸分析是什么
C++静态内存分配
int *foo(){
int t = 3;
return &t;
}
陷阱:t这个局部变量是在栈上分配的(即静态内存分配),函数执行完毕,变量占据的内存被销毁,任何对这个返回值的操作(如解引用)都将扰乱程序运行。
C++动态内存分配
int *foo(){
int *t = new int;
*t = 3;
return t
}
但是还是有一个问题,调用者可能会忘记删除或者直接将返回值传给其他函数,之后就再也不能删除它了,即发生内存泄漏。
Go语言中
func foo() *int{
t := new(int)
*t = 3
return t
}
没有任何问题
在编译原理中,分析指针动态范围的方法被称之为逃逸分析通俗来讲,当一个对象的指针被多个方法或线程引用时,则称这个指针发生了逃逸。逃逸分析决定一个变量是分配在堆上还是栈上。
1.2 逃逸分析有什么用
逃逸分析把变量合理分配到他该去的地方,内存在退出函数之后就没使用,那就分配到栈上;在函数之外还有其他的地方引用,就分配到堆上。”按需分配“。
堆和栈相比,堆适合不可预知大小的内存分配。但是分配速度较慢,且会形成内存碎片。栈内存分配非常快,只需要PUSH指令,并会自动释放;而堆分配内存首先需要找到一个大小合适的内存块,之后需要通过垃圾回收才能释放,垃圾回收会占用比较大的系统开销。
通过逃逸分析,可以尽量把不需要分配到堆的变量直接分配到栈,堆上的变量少了会减轻堆内存分配的开销,同时减少垃圾回收的压力
1.3 逃逸分析是怎么完成的
基本原则:如果一个函数返回对一个变量的引用,那么这个变量就会发生逃逸。
1)如果变量在函数外部没有引用,则优先放到栈上
2)如果变量在函数外部存在引用,则必定放到堆上
针对第一条,放到堆上的情形:定义了一个很大数组,需要申请的内存过大,超过了栈的存储能力。
1.4 如何确定是否发生逃逸
package main
import "fmt"
// 返回值
func f1() int {
t1 := 1 // 不发生逃逸
return t1
}
// 返回引用,指针
func f2() *int {
t2 := 3 // 逃逸到堆上
return &t2
}
// interface{}动态类型逃逸
func f3(x3 *int) {
fmt.Println(*x3) // *x3逃逸到堆
}
func main() {
x1 := f1()
fmt.Println(x1) // x1逃逸到堆
x2 := f2()
fmt.Println(*x2) // *x2逃逸到堆
f3(x2)
}
-gcflags用于启用编译器支持的额外标志
-m 用于输出编译器的优化细节(包括使用逃逸分析这种优化)
-l 用于禁用函数的内联优化,防止逃逸被编译器通过内联彻底的抹除
PS E:\go\gopher\第1章_逃逸分析> go build -gcflags '-m -l' .\demo01.go
# command-line-arguments
.\demo01.go:13:2: moved to heap: t2
.\demo01.go:19:9: x3 does not escape
.\demo01.go:20:13: ... argument does not escape
.\demo01.go:20:14: *x3 escapes to heap
.\demo01.go:25:13: ... argument does not escape
.\demo01.go:25:13: x1 escapes to heap
.\demo01.go:27:13: ... argument does not escape
.\demo01.go:27:14: *x2 escapes to heap
fmp.Println(a ...interface{})
,编译期间很难确定其参数的具体类型,也会发生逃逸
1.5 Go与C/C++中的堆和栈是同一个概念吗
-
C/C++中提及的"程序堆栈"本质上是操作系统层级的概念
-
Go程序也是运行在操作系统上的程序,但区别在于传统意义上的"栈"被Go语言运行时全部消耗了,用于维护运行时各个组件之间的协调,调度器\垃圾回收\系统调用等.
-
对于用户态的Go代码而言,所消耗的"堆和栈"是GO运行时通过管理向系统申请的堆内存,构造逻辑上的"堆和栈",本质都是从操作系统申请而来的堆内存.所以Go程序拥有"几乎"无限的栈内存(1GB),
-
对于用户态GO代码消耗的栈,GO语言运行时会为了防止内存碎片化,会在适当时候对整个栈进行深拷贝,将其整个复制到另一快内存区域,这也是相较于传统意义上栈是一块固定分配好的内存所出现的另一处差异.
-
由于这个特点,指针的运算不再凑效,因为再没有特殊说明的情况下,无法确定前后指针所指向的地址的内容是否已经被GO运行时移动.