Golang内存逃逸

Golang内存逃逸,第1张

逃逸分析

内存逃逸:栈上的内存逃逸到了堆上的现象就称为内存逃逸

概念

程序在编译阶段根据代码来确认哪些变量分配在栈区,哪些变量分配在堆区。这样可以防止过多内存在堆上分配,减轻GC压力以及程序STW的时间

原理

指向栈对象的指针不能存储在堆中

指向栈对象的指针不能超过该对象的存活期,也就是指针不能再栈对象被销毁后依然存活

如何查看

通过如下方式编译代码可以看到逃逸分析

go build -gcflags='-m -m -l' 
-m -m 能看到所有编译器优化-l 禁用掉内联优化 分析例子 函数返回局部变量指针
package main

type st struct {
    a int
    b int
}

func Add(x, y int) *st {
    r := st{a: x, b: y}
    return &r
}

func main() {
    res := Add(3, 5)
    res.a = 10
}

查看逃逸分析结果

$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:9:2: r escapes to heap:
./test.go:9:2:   flow: ~r2 = &r:
./test.go:9:2:     from &r (address-of) at ./test.go:10:9
./test.go:9:2:     from return &r (return) at ./test.go:10:2
./test.go:9:2: moved to heap: r
note: module requires Go 1.17

可以看到局部变量指针r发生了内存逃逸

函数返回局部变量
package main

type st struct {
    a int
    b int
}

func Add(x, y int) st {
    r := st{a: x, b: y}
    return r
}

func main() {
    res := Add(3, 5)
    res.a = 10
}

逃逸分析

$ go build -gcflags="-m -m -l" test.go
$
没有任何信息,说明没有发生逃逸 interface类型逃逸
package main

import "fmt"

func main() {
    res := 10
    fmt.Print(res)
}

逃逸分析

$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:7:11: res escapes to heap:
./test.go:7:11:   flow: {storage for ... argument} = &{storage for res}:
./test.go:7:11:     from res (spill) at ./test.go:7:11
./test.go:7:11:     from ... argument (slice-literal-element) at ./test.go:7:11
./test.go:7:11:   flow: {heap} = {storage for ... argument}:
./test.go:7:11:     from ... argument (spill) at ./test.go:7:11
./test.go:7:11:     from fmt.Print(... argument...) (call parameter) at ./test.go:7:11
./test.go:7:11: ... argument does not escape
./test.go:7:11: res escapes to heap
note: module requires Go 1.17

可以看到res发生了逃逸,这是因为:

编译器很难确定interface{}的具体类型,所以会发生逃逸

但是我们可以看到res本身没有move to heap,这是因为编译器只是把res存储的值存储到了堆区,res本身依然还是在栈区,但是我们可以做以下修改:

package main

import "fmt"

func main() {
    res := 10
    fmt.Print(&res)
}

逃逸分析

$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:6:2: res escapes to heap:
./test.go:6:2:   flow: {storage for ... argument} = &res:
./test.go:6:2:     from &res (address-of) at ./test.go:7:12
./test.go:6:2:     from &res (interface-converted) at ./test.go:7:12
./test.go:6:2:     from ... argument (slice-literal-element) at ./test.go:7:11
./test.go:6:2:   flow: {heap} = {storage for ... argument}:
./test.go:6:2:     from ... argument (spill) at ./test.go:7:11
./test.go:6:2:     from fmt.Print(... argument...) (call parameter) at ./test.go:7:11
./test.go:6:2: moved to heap: res
./test.go:7:11: ... argument does not escape
note: module requires Go 1.17

会发现,res本身也逃逸到了堆区,这是因为fmt.Print输出的是res的地址,因此编译器会将地址的值存储到了堆区,但是堆上的对象不能存储一个栈上的地址,因为栈变量销毁后,其地址就无效了,那堆区中存储的地址也会无效,因此需要将res也逃逸到堆区。

闭包产生的逃逸
package main

func add() func() int {
    num := 0
    f := func() int {
        num++
        return num
    }
    return f
}

func main() {
    add()
}

逃逸分析:

$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:6:3: add.func1 capturing by ref: num (addr=true assign=true width=8)
./test.go:5:7: func literal escapes to heap:
./test.go:5:7:   flow: f = &{storage for func literal}:
./test.go:5:7:     from func literal (spill) at ./test.go:5:7
./test.go:5:7:     from f := func literal (assign) at ./test.go:5:4
./test.go:5:7:   flow: ~r0 = f:
./test.go:5:7:     from return f (return) at ./test.go:9:2
./test.go:4:2: num escapes to heap:
./test.go:4:2:   flow: {storage for func literal} = &num:
./test.go:4:2:     from func literal (captured by a closure) at ./test.go:5:7 
./test.go:4:2:     from num (reference) at ./test.go:6:3
./test.go:4:2: moved to heap: num
./test.go:5:7: func literal escapes to heap
note: module requires Go 1.17

可以看到f和闭包中的num都发生了内存逃逸。因为函数也是一个指针类型,因此当做返回值时也发生了逃逸。而只要调用了f,就会调用num,因此num也需要逃逸到堆区

变量大小不确定或者栈空间不足 当栈空间足够时,不会发生逃逸,但是当变量过大时,已经完全超过栈空间的大小时,将会发生逃逸到堆上分配内存初始化切片时,没有直接指定大小,而是填入的变量,这种情况为了保证内存的安全,编译器也会触发逃逸,在堆上进行分配内存 向channel发送指针数据
package main

func main() {
    ch1 := make(chan *int, 1)
    y := 5
    py := &y
    ch1 <- py
}

逃逸分析

$ go build -gcflags="-m -m -l" test.go
# command-line-arguments
./test.go:5:2: y escapes to heap:
./test.go:5:2:   flow: py = &y:
./test.go:5:2:     from &y (address-of) at ./test.go:6:8
./test.go:5:2:     from py := &y (assign) at ./test.go:6:5
./test.go:5:2:   flow: {heap} = py:
./test.go:5:2:     from ch1 <- py (send) at ./test.go:7:6
./test.go:5:2: moved to heap: y
note: module requires Go 1.17

编译器无法知道channel 中的数据会被哪个 goroutine 接收,无法知道什么时候释放

总结 逃逸分析在编译阶段确定哪些变量可以分配在栈上,哪些变量分配在堆上减轻了GC压力,提供程序的运行速度栈上内存使用完毕不需要GC处理,堆上内存使用完毕会交给GC处理尽量减少逃逸代码,减轻GC压力

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/995958.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存