共计 5109 个字符,预计需要花费 13 分钟才能阅读完成。
总结
- 逃逸的指针变量自身也是调配在堆空间,故函数能够返回局部变量的地址。此时的局部变量相当于部分指针变量,逃逸时,指针变量自身也是调配在堆空间,所以能返回它的地址。
- 栈空间的内存由编译器治理,调配开释速度很快。堆空间,由 gc 治理,频繁的 gc 会占用零碎较大的开销,stop the world
- 逃逸剖析是编译器在动态编译时实现的。
- 切片变量自身逃逸了,那它底层的 data 区域也会逃逸。即便切片长度很小。
- 切片变量自身没逃逸,那个别状况它的 data 区域也在栈上,若长度太长,则 data 区域会调配到堆上,但切片变量自身还是在栈上。
如何确定内存逃逸?
go run -gcflags '-m -l' main.go
留神:上述命令只在编译器接入判断是否逃逸处有输入。
有时候栈空间比堆空间地址还小,不晓得为啥。
内存逃逸的例子
func main() {
a := 1
_ = a
}
// 无输入
func main() {
a := 1
fmt.Printf("%p\n", &a)
fmt.Println(a)
b := 2
fmt.Printf("%p\n", &b)
fmt.Println(b)
c := 3
fmt.Printf("%p\n", &c)
fmt.Println(c)
d := 4
fmt.Printf("%p\n", &d)
fmt.Println(d)
}
// 输入
./main.go:10:2: moved to heap: a
./main.go:14:2: moved to heap: b
./main.go:18:2: moved to heap: c
./main.go:22:2: moved to heap: d
./main.go:11:12: ... argument does not escape
./main.go:12:13: ... argument does not escape
./main.go:12:13: a escapes to heap
./main.go:15:12: ... argument does not escape
./main.go:16:13: ... argument does not escape
./main.go:16:13: b escapes to heap
./main.go:19:12: ... argument does not escape
./main.go:20:13: ... argument does not escape
./main.go:20:13: c escapes to heap
./main.go:23:12: ... argument does not escape
./main.go:24:13: ... argument does not escape
./main.go:24:13: d escapes to heap
0xc000012080
1
0xc000012088
2
0xc0000120a0
3
0xc0000120a8
4
fmt.Printf 函数 abc 赋值给 interface{},造成逃逸,abc 地址在堆空间,如图正增长,印证了在堆上。比照上面:
func main() {
a := 1
println(&a)
println(a)
b := 2
println(&b)
println(b)
c := 3
println(&c)
println(c)
d := 4
println(&d)
println(d)
}
// 输入
0xc00002e768
1
0xc00002e760
2
0xc00002e758
3
0xc00002e750
4
println 不会触发内存逃逸,abc 调配在栈空间,地址从高到低增长印证了这点。
四个指针没逃逸的状况:
func main() {a := new(int)
println(&a)
println(a)
println("======")
b := new(int)
println(&b)
println(b)
println("======")
c := new(int)
println(&c)
println(c)
println("======")
d := new(int)
println(&d)
println(d)
}
// 输入
./main.go:32:10: new(int) does not escape
./main.go:39:10: new(int) does not escape
./main.go:46:10: new(int) does not escape
./main.go:53:10: new(int) does not escape
0xc00002e768
0xc00002e740
======
0xc00002e760
0xc00002e738
======
0xc00002e758
0xc00002e730
======
0xc00002e750
0xc00002e748
abcd 指针变量自身和其所指的内存区域,没有逃逸,都是调配在栈空间,打印的地址从大到小为间断区域印证了这点。
内存调配如图:
四个指针有逃逸的状况
func main() {a := new(int)
fmt.Printf("%p\n", &a)
fmt.Println(a)
println("======")
b := new(int)
fmt.Printf("%p\n", &b)
fmt.Println(b)
println("======")
c := new(int)
fmt.Printf("%p\n", &c)
fmt.Println(c)
println("======")
d := new(int)
fmt.Printf("%p\n", &d)
fmt.Println(d)
println("======")
f := new(int)
println(&f)
println(f)
}
// 输入
0xc00000e028
0xc000012080
======
0xc00000e038
0xc000012088
======
0xc00000e040
0xc0000120a0
======
0xc00000e048
0xc0000120a8
======
0xc000058f38
0xc000058f30
逃逸状况下,abcd 指针变量自身和其所指的内存区域,都是调配在堆空间,打印的地址从小到大为间断区域印证了这点。f 指针变量和其所指的内存区域调配在栈空间,其地址高于堆区域地址印证了这点。
逃逸的指针变量自身也是调配在堆空间,故函数能够返回部分指针变量
内存调配图如下:
func main() {a := new(int)
_ = a
}
// 输入
./main.go:8:10: main new(int) does not escape
即便是 new 的变量,没逃逸时,也是调配在栈
- println 函数不会触发逃逸,而 fmt.Printf 会,因为它的函数参数是 interface{}类型
- 留神: 下面例子,a 是指针变量调配在栈空间,指向的 int 存储单元在下一个字节(也是在栈空间)。
- 栈空间在高地址为,从高往低调配。
这要是在 C ++ 中这么写,是个很典型的谬误:返回局部变量的地址,该地址的内容在函数退出后会被主动开释,因为是在栈上的。
那么 go 语言的局部变量到底是在栈上还是堆上呢?go 语言编译器会做逃逸剖析(escape analysis),剖析局部变量的作用域是否逃出函数的作用域,要是没有,那么就放在栈上;要是变量的作用域超出了函数的作用域,那么就主动放在堆上。
测试察看 g 的地址变动
func main {
var g *int
println(&g)
println(g)
g = new(int)
println(&g)
println(g)
g = new(int)
println(&g)
println(g)
g = new(int)
fmt.Println(&g)
fmt.Println(g)
}
没有最初两行的话,没逃逸,g 与 g 指向的内存都在栈区,加了则都在堆区。
呈现逃逸的场景
package main
type Student struct {Name interface{}
}
func main() {stu := new(Student)
stu.Name = "tom"
}
interface{} 赋值,会产生逃逸,优化计划是将类型设置为固定类型,例如:string
package main
type Student struct {Name string}
func GetStudent() *Student {stu := new(Student)
stu.Name = "tom"
return stu
}
func main() {GetStudent()
}
返回(局部变量的地址)指针类型,会产生逃逸,优化计划视状况而定。
函数传递指针和传值哪个效率高吗?咱们晓得传递指针能够缩小底层值的拷贝,能够提高效率,然而如果拷贝的数据量小,因为指针传递会产生逃逸,可能会应用堆,也可能会减少 GC 的累赘,所以传递指针不肯定是高效的。
不要自觉应用变量指针作为参数,尽管缩小了复制,但变量逃逸的开销可能更大。
func main() {nums := make([]int, 5, 5)
nums2 := make([]int, 5, 5)
println(&nums)
println(nums)
println(&nums2)
println(nums2)
for i := range nums {nums[i] = i
println(&nums[i])
}
println("======2")
for i := range nums2 {nums2[i] = i
println(&nums2[i])
}
println("======3")
nums3 := make([]int, 10000, 10000)
println(&nums3)
println(nums3)
for i := range nums3 {
if i == 5 {break}
nums3[i] = i
println(&nums3[i])
}
//fmt.Println(&nums3)
}
// 输入
./main.go:8:14: make([]int, 5, 5) does not escape
./main.go:9:15: make([]int, 5, 5) does not escape
./main.go:25:15: make([]int, 10000, 10000) escapes to heap
0xc00002e758
[5/5]0xc00002e6f8
0xc00002e740
[5/5]0xc00002e6d0
0xc00002e6f8
0xc00002e700
0xc00002e708
0xc00002e710
0xc00002e718
======2
0xc00002e6d0
0xc00002e6d8
0xc00002e6e0
0xc00002e6e8
0xc00002e6f0
======3
0xc00002e728
[10000/10000]0xc000054000
0xc000054000
0xc000054008
0xc000054010
0xc000054018
0xc000054020
如上:第 1、2 个 slice 变量及其底层的 data 指针区域,都是调配在栈上。
第 3 个超大切片,slice 变量自身调配在栈上,底层数据在堆上。
如果正文掉最初的 fmt.Printf 则第 3 个 slice 变量自身也会逃逸调配在堆上。
留神:切片 data 区域第 1 到第 n 个元素的地址空间都是从 0 往上增的,不论是在栈还是在堆上。
func main() {var slice1 []int
println(&slice1)
println(slice1)
fmt.Printf("%p\n", &slice1)
println(&slice1)
println(slice1)
slice1 = make([]int,5,5)
println(&slice1)
println(slice1)
}
// 输入
./main.go:11:6: moved to heap: slice1
./main.go:14:12: ... argument does not escape
./main.go:17:15: make([]int, 5, 5) escapes to heap
0xc0000a4018
[0/0]0x0
0xc0000a4018
0xc0000a4018
[0/0]0x0
0xc0000a4018
[5/5]0xc0000aa030
切片变量自身逃逸了,那它底层的 data 区域也会逃逸。即便切片长度很小。
切片变量自身没逃逸,那个别状况它的 data 区域也在栈上,若长度太长,则 data 区域会调配到堆上,但切片变量自身还是在栈上。
package main
func main() {nums := make([]int, 10000, 10000)
for i := range nums {nums[i] = i
}
}
切片太大,会产生逃逸,优化计划尽量设置容量,如果容量切实过大那就没方法了。
map 里的元素是不能取地址的。
map 没逃逸时,也是调配在栈上的,变量自身及底层的数据区。