逃逸分析发生在编译阶段,是指由编译器决定变量被分配在栈区还是堆区。如果分配在栈区,当函数执行完成后内存自动释放;如果分配在堆区,函数执行完成后由垃圾回收机制释放内存。
不管是字符串、数组,还是通过new、make标识符创建的对象都由逃逸分析来决定是分配在栈区还是堆区。
逃逸策略
-
变量的使用范围:如果函数中的变量没有被函数外部引用,则优先分配到栈中;如果函数中的变量被函数外部引用,则分配到堆中;
-
变量占用的内存大小:如果栈空间不足,则变量分配到堆中。
-
变量类型是否确定:编译期不能确定变量的具体类型,则分配到堆中。
-
变量长度是否确定:编译期不能确定长度的变量,则分配到堆中。
场景分析
通过编译器命令go build -gcflags可以查看程序是否有逃逸现象发生:
go build -gcflags '-m -l' xxx.go
// 查看详细信息
go build -gcflags '-m -m -l' xxx.go
下面我们通过例子来分析逃逸策略的场景,通过以上命令来查看逃逸结果。
变量的使用范围
变量的使用范围包括指针逃逸、引用类型逃逸和闭包引用变量逃逸。
指针逃逸
func ptr() *int {
i := 1
return &i
}
func ptrEscape() {
ptr()
}
逃逸分析:
go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:4:2: i escapes to heap:
./escape.go:4:2: flow: ~r0 = &i:
./escape.go:4:2: from &i (address-of) at ./escape.go:5:9
./escape.go:4:2: from return &i (return) at ./escape.go:5:2
./escape.go:4:2: moved to heap: i
从结果可以看出变量i已经逃逸到堆中。
引用类型逃逸
func ref() []int {
ai := []int{1, 2, 3, 4, 5}
return ai
}
func refEscape() {
ref()
}
逃逸分析:
go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:13:13: []int{...} escapes to heap:
./escape.go:13:13: flow: ai = &{storage for []int{...}}:
./escape.go:13:13: from []int{...} (spill) at ./escape.go:13:13
./escape.go:13:13: from ai := []int{...} (assign) at ./escape.go:13:5
./escape.go:13:13: flow: ~r0 = ai:
./escape.go:13:13: from return ai (return) at ./escape.go:14:2
./escape.go:13:13: []int{...} escapes to heap
由于切片结构体中包含指向底层数组的指针,因此切片[]i也逃逸到了堆中。
闭包引用变量逃逸
func closure() func() int {
i := 0
return func() int {
i++
return i
}
}
func closureEscape() {
closure()
}
逃逸分析:
go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:24:3: closure.func1 capturing by ref: i (addr=true assign=true width=8)
./escape.go:23:9: func literal escapes to heap:
./escape.go:23:9: flow: ~r0 = &{storage for func literal}:
./escape.go:23:9: from func literal (spill) at ./escape.go:23:9
./escape.go:23:9: from return func literal (return) at ./escape.go:23:2
./escape.go:22:2: i escapes to heap:
./escape.go:22:2: flow: {storage for func literal} = &i:
./escape.go:22:2: from func literal (captured by a closure) at ./escape.go:23:9
./escape.go:22:2: from i (reference) at ./escape.go:24:3
./escape.go:22:2: moved to heap: i
./escape.go:23:9: func literal escapes to heap
变量占用的内存大小
func memOccupy() {
s := make([]int, 1000)
for i := range s {
s[i] = i
}
}
逃逸分析:
go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:34:11: make([]int, 1000) does not escape
当切片的长度为1000时没有发生逃逸,我们把长度放大10倍看看:
func memOccupy() {
s := make([]int, 10000)
for i := range s {
s[i] = i
}
}
逃逸分析:
go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:34:11: make([]int, 10000) escapes to heap:
./escape.go:34:11: flow: {heap} = &{storage for make([]int, 10000)}:
./escape.go:34:11: from make([]int, 10000) (too large for stack) at ./escape.go:34:11
./escape.go:34:11: make([]int, 10000) escapes to heap
从分析结果“(too large for stack)”看出,切片需要分配的内存太大,栈空间不足,因此逃逸到了堆中。
变量类型不确定
func typeEscape() {
i := 0
fmt.Println(i)
}
逃逸分析:
go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:44:13: i escapes to heap:
./escape.go:44:13: flow: {storage for ... argument} = &{storage for i}:
./escape.go:44:13: from i (spill) at ./escape.go:44:13
./escape.go:44:13: from ... argument (slice-literal-element) at ./escape.go:44:13
./escape.go:44:13: flow: {heap} = {storage for ... argument}:
./escape.go:44:13: from ... argument (spill) at ./escape.go:44:13
./escape.go:44:13: from fmt.Println(... argument...) (call parameter) at ./escape.go:44:13
./escape.go:44:13: ... argument does not escape
./escape.go:44:13: i escapes to heap
由于fmt.Println()的参数类型是interface{},在编译器不能确定其参数类型,因此逃逸到堆中。
变量长度不确定
func varLenEscape() {
len := 10
s := make([]int, 1, len)
s[0] = 1
}
逃逸分析:
go build -gcflags '-m -m -l' main.go escape.go
# command-line-arguments
./escape.go:47:11: make([]int, 1, len) escapes to heap:
./escape.go:47:11: flow: {heap} = &{storage for make([]int, 1, len)}:
./escape.go:47:11: from make([]int, 1, len) (non-constant size) at ./escape.go:47:11
./escape.go:47:11: make([]int, 1, len) escapes to heap
输出结果“ (non-constant size)”,变量长度不确定逃逸到堆中。
总结
逃逸分析发生在编译阶段,由编译器决定变量分配在栈区还是堆区。逃逸策略分为四种情况:根据变量的使用范围进行分析;根据变量占用内存的大小进行分析;根据变量的类型是否确定进行分析;根据变量的大小是否确定进行分析。
更多【分布式专辑】【架构实战专辑】系列文章,请关注公众号:coding到灯火阑珊