Golang内存逃逸

逃逸分析

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

概念

程序在编译阶段根据代码来确认哪些变量分配在栈区,哪些变量分配在堆区。这样可以防止过多内存在堆上分配,减轻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压力
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值