Go内存逃逸
Go内存逃逸
简单来说就是原本应在栈上分配内存的对象,逃逸到了堆上进行分配。如果能在栈上进行分配,那么只需要两个指令,入栈和出栈,GC压力也小了。所以相比之下,在栈上分配代价会小很多。
go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。
指针逃逸
package main
func foo(arg_val int)(*int) {
var foo_val int = 11;
return &foo_val;
}
func main() {
main_val := foo(666)
println(*main_val)
}
package main
type Student struct {
Name string
Age int
}
func StudentRegister(name string, age int) *Student {
s := new(Student) //局部变量s逃逸到堆
s.Name = name
s.Age = age
return s
}
func main() {
StudentRegister("Jim", 18)
}
栈空间不足逃逸(空间开辟过大)
package main
func Slice() {
s := make([]int, 10000, 10000)
for index, _ := range s {
s[index] = index
}
}
func main() {
Slice()
}
动态类型逃逸(不确定长度大小)
func main() {
s := "Escape"
fmt.Println(s)
}
又或者像前面提到的例子:
func F() {
a := make([]int, 0, 20) // 栈 空间小
b := make([]int, 0, 20000) // 堆 空间过大 逃逸
l := 20
c := make([]int, 0, l) // 堆 动态分配不定空间 逃逸
}
func main() {
data := []interface{}{100, 200}
data[0] = 100
}
//结果
aceld:test ldb$ go tool compile -m 1.go
1.go:3:6: can inline main
1.go:4:23: []interface {}{...} does not escape
1.go:4:24: 100 does not escape
1.go:4:29: 200 does not escape
1.go:6:10: 100 escapes to heap
func main() {
data := make(map[string]interface{})
data["key"] = 200
}
aceld:test ldb$ go tool compile -m 2.go
2.go:3:6: can inline main
2.go:4:14: make(map[string]interface {}) does not escape
2.go:6:14: 200 escapes to heap
func main() {
data := make(map[interface{}]interface{})
data[100] = 200
}
//
aceld:test ldb$ go tool compile -m 3.go
3.go:3:6: can inline main
3.go:4:14: make(map[interface {}]interface {}) does not escape
3.go:6:6: 100 escapes to heap
3.go:6:12: 200 escapes to heap
闭包引用对象逃逸
func Fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
func main() {
f := Fibonacci()
for i := 0; i < 10; i++ {
fmt.Printf("Fibonacci: %d\n", f())
}
}
引用对象逃逸
package main
func main() {
data := make(map[string][]string)
data["key"] = []string{"value"}
}
我们通过编译看看逃逸结果
aceld:test ldb$ go tool compile -m 4.go
4.go:3:6: can inline main
4.go:4:14: make(map[string][]string) does not escape
4.go:6:24: []string{...} escapes to heap
//因为map的值为[]string切片,由于切片为引用值类型,所以一开始编译不确定
package main
func main() {
a := 10
data := []*int{nil}
data[0] = &a
}
我们通过编译看看逃逸结果
aceld:test ldb$ go tool compile -m 5.go
5.go:3:6: can inline main
5.go:4:2: moved to heap: a
5.go:6:16: []*int{...} does not escape
//因为data存的是引用类型值,所以a发生逃逸
package main
func main() {
ch := make(chan []string)
s := []string{"aceld"}
go func() {
ch <- s
}()
}
我们通过编译看看逃逸结果
aceld:test ldb$ go tool compile -m 8.go
8.go:8:5: can inline main.func1
8.go:6:15: []string{...} escapes to heap
8.go:8:5: func literal escapes to heap
我们看到[]string{...} escapes to heap
, s被逃逸到堆上。
形参为引用类型
func(*int)
函数类型,进行函数赋值,会使传递的形参出现逃逸现象,形参为引用类型。
package main
import "fmt"
func foo(a *int) {
return
}
func main() {
data := 10
f := foo
f(&data)
fmt.Println(data)
}
我们通过编译看看逃逸结果
aceld:test ldb$ go tool compile -m 6.go
6.go:5:6: can inline foo
6.go:12:3: inlining call to foo
6.go:14:13: inlining call to fmt.Println
6.go:5:10: a does not escape
6.go:14:13: data escapes to heap
6.go:14:13: []interface {}{...} does not escape
:1: .this does not escape
我们会看到data已经被逃逸到堆上。
func([]string)
: 函数类型,进行[]string{"value"}
赋值,会使传递的参数出现逃逸现象。
package main
import "fmt"
func foo(a []string) {
return
}
func main() {
s := []string{"aceld"}
foo(s)
fmt.Println(s)
}
我们通过编译看看逃逸结果
aceld:test ldb$ go tool compile -m 7.go
7.go:5:6: can inline foo
7.go:11:5: inlining call to foo
7.go:13:13: inlining call to fmt.Println
7.go:5:10: a does not escape
7.go:10:15: []string{...} escapes to heap
7.go:13:13: s escapes to heap
7.go:13:13: []interface {}{...} does not escape
:1: .this does not escape
我们看到 s escapes to heap
,s被逃逸到堆上。
逃逸总结
- 栈上分配内存比在堆中分配内存有更高的效率
- 栈上分配的内存不需要GC处理
- 堆上分配的内存使用完毕会交给GC处理
- 逃逸分析目的是决定内分配地址是栈还是堆
- 逃逸分析在编译阶段完成