总结

  • 逃逸的指针变量自身也是调配在堆空间,故函数能够返回局部变量的地址。此时的局部变量相当于部分指针变量,逃逸时,指针变量自身也是调配在堆空间,所以能返回它的地址。
  • 栈空间的内存由编译器治理,调配开释速度很快。堆空间,由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 heap0xc00001208010xc00001208820xc0000120a030xc0000120a84

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)}// 输入0xc00002e76810xc00002e76020xc00002e75830xc00002e7504

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 escape0xc00002e7680xc00002e740======0xc00002e7600xc00002e738======0xc00002e7580xc00002e730======0xc00002e7500xc00002e748

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)}//输入0xc00000e0280xc000012080======0xc00000e0380xc000012088======0xc00000e0400xc0000120a0======0xc00000e0480xc0000120a8======0xc000058f380xc000058f30

逃逸状况下,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 maintype Student struct { Name interface{}}func main()  { stu := new(Student) stu.Name = "tom"}

interface{} 赋值,会产生逃逸,优化计划是将类型设置为固定类型,例如:string

package maintype 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 heap0xc00002e758[5/5]0xc00002e6f80xc00002e740[5/5]0xc00002e6d00xc00002e6f80xc00002e7000xc00002e7080xc00002e7100xc00002e718======20xc00002e6d00xc00002e6d80xc00002e6e00xc00002e6e80xc00002e6f0======30xc00002e728[10000/10000]0xc0000540000xc0000540000xc0000540080xc0000540100xc0000540180xc000054020

如上:第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 heap0xc0000a4018[0/0]0x00xc0000a40180xc0000a4018[0/0]0x00xc0000a4018[5/5]0xc0000aa030

切片变量自身逃逸了,那它底层的data区域也会逃逸。即便切片长度很小。
切片变量自身没逃逸,那个别状况它的data区域也在栈上,若长度太长,则data区域会调配到堆上,但切片变量自身还是在栈上。

package mainfunc main() { nums := make([]int, 10000, 10000) for i := range nums {  nums[i] = i }}

切片太大,会产生逃逸,优化计划尽量设置容量,如果容量切实过大那就没方法了。

map里的元素是不能取地址的。

map没逃逸时,也是调配在栈上的,变量自身及底层的数据区。