Go内存管理逃逸分析

1. 前言

所谓的逃逸分析(Escape analysis)是指由编译器决定内存分配的位置吗不需要程序员指定。


函数中申请一个新的对象

  • 如果分配在栈中, 则函数执行结束后可自动将内存回收
  • 如果分配在堆中, 则函数执行借宿可交给GC(垃圾回收)处理

有了逃逸分析,返回函数局部变量将变得可能,除此之外,逃逸分析还跟闭包息息相关,了解哪些场景下对象会逃逸至关重要。


2. 逃逸策略

每当函数中申请新的对象,编译器会根据该对象是否被函数外部引用来决定是否逃逸:

  1. 如果函数外部没有引用,则优先放到栈中;
  2. 如果函数外部存在引用,则必定放到堆中;

注意,对于函数外部没有引用的对象,也有可能放到堆中,比如内存过大超过栈的存储能力。


3. 逃逸场景

3.1 指针逃逸

我们知道Go可以返回局部变量指针,这其实是一个典型的变量逃逸案例,示例代码如下:

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)
}

函数StudentRegister()内部s为局部变量,其值通过函数返回值返回,s本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。

通过编译参数-gcflag=-m可以查看编译过程中的逃逸分析:

D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:8: can inline StudentRegister
.\main.go:17: can inline main
.\main.go:18: inlining call to StudentRegister
.\main.go:8: leaking param: name
.\main.go:9: new(Student) escapes to heap
.\main.go:18: main new(Student) does not escape

可见在StudentRegister()函数中,也即代码第9行显示”escapes to heap”,代表该行内存分配发生了逃逸现象。

3.2 栈空间不足逃逸

看下面的代码,是否会产生逃逸呢?

package main

func Slice() {
    s := make([]int, 1000, 1000)

    for index, _ := range s {
        s[index] = index
    }
}

func main() {
    Slice()
}

上面代码Slice()函数中分配了一个1000个长度的切片,是否逃逸取决于栈空间是否足够大。
直接查看编译提示,如下:

D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:4: Slice make([]int, 1000, 1000) does not escape

我们发现此处并没有发生逃逸。那么把切片长度扩大10倍即10000会如何呢?

D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:4: make([]int, 10000, 10000) escapes to heap

我们发现当切片长度扩大到10000时就会逃逸。

实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

3.3 动态类型逃逸

很多函数参数为interface类型,比如fmt.Println(a …interface{}),编译期间很难确定其参数的具体类型,也会产生逃逸。
如下代码所示:

package main

import "fmt"

func main() {
    s := "Escape"
    fmt.Println(s)
}

上述代码s变量只是一个string类型变量,调用fmt.Println()时会产生逃逸:

D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:7: s escapes to heap
.\main.go:7: main ... argument does not escape

3.4 闭包引用对象逃逸

某著名的开源框架实现了某个返回Fibonacci数列的函数:

func Fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

该函数返回一个闭包,闭包引用了函数的局部变量a和b,使用时通过该函数获取该闭包,然后每次执行闭包都会依次输出Fibonacci数列。
完整的示例程序如下所示:

package main

import "fmt"

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())
    }
}

上述代码通过Fibonacci()获取一个闭包,每次执行闭包就会打印一个Fibonacci数值。输出如下所示:

D:\SourceCode\GoExpert\src>src.exe
Fibonacci: 1
Fibonacci: 1
Fibonacci: 2
Fibonacci: 3
Fibonacci: 5
Fibonacci: 8
Fibonacci: 13
Fibonacci: 21
Fibonacci: 34
Fibonacci: 55

Fibonacci()函数中原本属于局部变量的a和b由于闭包的引用,不得不将二者放到堆上,以致产生逃逸:

D:\SourceCode\GoExpert\src>go build -gcflags=-m
# _/D_/SourceCode/GoExpert/src
.\main.go:7: can inline Fibonacci.func1
.\main.go:7: func literal escapes to heap
.\main.go:7: func literal escapes to heap
.\main.go:8: &a escapes to heap
.\main.go:6: moved to heap: a
.\main.go:8: &b escapes to heap
.\main.go:6: moved to heap: b
.\main.go:17: f() escapes to heap
.\main.go:17: main ... argument does not escape

4 逃逸总结

  • 栈上分配内存比在堆中分配内存有更高的效率
  • 栈上分配的内存不需要GC处理
  • 堆上分配的内存使用完毕会交给GC处理
  • 逃逸分析目的是决定内分配地址是栈还是堆
  • 逃逸分析在编译阶段完成

5. 注意事项

思考一下这个问题:函数传递指针真的比传值效率高吗?
我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

0 条评论
请不要发布违法违规有害信息,如发现请及时举报或反馈
还没有人评论呢,速度抢占沙发!
相关文章
  • 学习:SQL 语句到结构体的转换 | Go 语言编程之旅 (eddycjy.com) 目标:SQL表转换为Go语言结构体 可以在线体验这个过程:SQL生成GO语言结构体 - 支持批量处理 (tl.be...

  • 引言 最近做了一个需求,是定时任务相关的。以前定时任务都是通过 linux crontab 去实现的,现在服务上云(k8s)了,尝试了 k8s 的 CronJob,由于公司提供的是界面化工具,使用、查...

  • 背景 本文使用 Golang语言的SDK包 go.etcd.io/etcd/clientv3 实践etcd的租约、Watch等功能,并且实现分布式锁的业务场景。 etcd 租约 etcd过期时间可以通...

  • 路由 f, h, err := c.GetFile("uploadFile") if err != nil { logx.Error("getfile err ", err) ...

  • 概述建议先阅读 非缓冲通道, 缓冲通道, 关闭通道,通道方向。Go 语言没有提供函数或方法判断一个通道是否关闭。因此只能使用一个变通的办法:接收通道元素,根据返回的布尔值确定通道是否关闭。例子双向通道...

  • 普通打印优点:内置函数,不需要引入额外的包,简单方便。不足:无法进行格式化打印,无法完整打印复合数据结构 (如数组, Map 等)。println 函数打印多个传入的参数,并自动加一个换行。例子pac...

  • 项目中的dao层,我们用来查询数据库,获取想要数据。有时我们会需要查询数据给结构体赋值,并返回一个结构体指针,如下 // 结构体字段已与数据库对应 func GetCommunity(id int) ...

  • 一、标识符 标识符是指定义的具有特殊意义的词,例如变量、常量、函数名等等,任何一门语言中都对自己的标识符有特殊定义的规则。在 Go 语言中,标识符由字母数字和下划线组成,并且只能以字母和下划线开头,例...

  • 背景 在go的工程中,有时init的顺序是至关重要的,本文写了一个小控制器去管理init的顺序,可以根据自己的要求设置不同的权重来实现加载顺序。 本文控制器主要实现两个功能,一是按照优先级加载包的引用...

  • GO语言的环境安装 下载地址 Go下载 - Go语言中文网 - Golang中文社区 (studygolang.com) 安装 这里很简单,可以一直点下一步就可以了 环境搭建 我这里也是去D盘当...

  • dongle 是一个轻量级、语义化、对开发者友好的 Golang 编码解码和加密解密库Dongle 已被 awesome-go 收录, 如果您觉得不错,请给个 star 吧github.com/gol...

  • 输入某二叉树的前序遍历和中序遍历的结果,请构建该二叉树并返回其根节点。 假设输入的前序遍历和中序遍历的结果中都不含重复的数字。 示例 1: Input: preorder = [3,9,20,...

  • 本文介绍了Go 微服务体系发展与选型,过去一年Dubbo-go 社区的飞速发展以及对未来的展望。 一、Go 微服务体系发展与选型   随着微服务技术的快速发展,其在各个领域都形成了一系列...

  • 最近为了让python语言能够直接调用PaddleOCR的C++的动态链接库,针对本人已经开源的PaddleOCR项目https://gitee.com/raoyutian/paddle-ocrsha...

  • 前提准备与运行环境请参考:   在 Go 中 for 用来循环和迭代, Go 语言没有 while,do,until 这几个关键字,我们只能使用 for。这也算是件好事! 让我们来为一...

  • 什么是传值(值传递) func main() { i:=10 ip:=&i fmt.Printf("原始指针的内存地址是:%p\n",&ip) modify(ip) fmt.Println(...

  • 一 Consul介绍 Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。 Consul是分布式的、高可用的、可横向扩展的。 1. 注册中心Consul基本介绍 ...

  • 概述建议先阅读 goroutine 小节。Go 箴言: 不要通过共享内存来通信,而要通过通信来共享内存。goroutine 是 Go 程序并发执行的实体,channel (通道) 则是它们之间的连接,...

  • hello,大家好呀,我是既写 Java 又写 Go 的小楼,在写 Go 的过程中经常对比这两种语言的特性,踩了不少坑,也发现了不少有意思的地方,今天就来聊聊 Go 自带的 HttpClient 的超...

  • 自定义类型关键字 type, 主要用来对同一种类型进行抽象。语法规则type 自定义类型名称 具体类型 # 例子 type Number int同时定义多个自定义类型package ma...