文章插图
前序(Prelude)本系列文章总共四篇,主要帮助大家理解 Go 语言中一些语法结构和其背后的设计原则,包括指针、栈、堆、逃逸分析和值/指针传递 。这是第二篇,主要介绍堆和逃逸分析 。
以下是本系列文章的索引:
- 「GCTT 出品」Go 语言机制之栈和指针
- Go 语言机制之逃逸分析
- Go 语言机制之内存剖析
- Go 语言机制之数据和语法的设计哲学
逃逸分析是编译器用来决定你的程序中值的位置的过程 。特别地,编译器执行静态代码分析,以确定一个构造体的实例化值是否会逃逸到堆 。在 Go 语言中,你没有可用的关键字或者函数,能够直接让编译器做这个决定 。只能够通过你写代码的方式来作出这个决定 。
堆(Heaps)
堆是内存的第二区域,除了栈之外,用来存储值的地方 。堆无法像栈一样能自清理,所以使用这部分内存会造成很大的开销(相比于使用栈) 。重要的是,开销跟 GC(垃圾收集),即被牵扯进来保证这部分区域干净的程序,有很大的关系 。当垃圾收集程序运行时,它会占用你的可用 CPU 容量的 25% 。更有甚者,它会造成微秒级的 “stop the world” 的延时 。拥有 GC 的好处是你可以不再关注堆内存的管理,这部分很复杂,是历史上容易出错的地方 。
在 Go 中,会将一部分值分配到堆上 。这些分配给 GC 带来了压力,因为堆上没有被指针索引的值都需要被删除 。越多需要被检查和删除的值,会给每次运行 GC 时带来越多的工作 。所以,分配算法不断地工作,以平衡堆的大小和它运行的速度 。
共享栈(Sharing Stacks)
在 Go 语言中,不允许 goroutine 中的指针指向另外一个 goroutine 的栈 。这是因为当栈增长或者收缩时,goroutine 中的栈内存会被一块新的内存替换 。如果运行时需要追踪指针指向其他的 goroutine 的栈,就会造成非常多需要管理的内存,以至于更新指向那些栈的指针将使 “stop the world” 问题更严重 。
这里有一个栈被替换好几次的例子 。看输出的第 2 和第 6 行 。你会看到 main 函数中的栈的字符串地址值改变了两次 。https://play.golang.org/p/pxn5u4EBSI
逃逸机制(Escape Mechanics)
任何时候,一个值被分享到函数栈帧范围之外,它都会在堆上被重新分配 。这是逃逸分析算法发现这些情况和管控这一层的工作 。(内存的)完整性在于确保对任何值的访问始终是准确、一致和高效的 。
通过查看这个语言机制了解逃逸分析 。https://play.golang.org/p/Y_VZxYteKO
清单 1
文章插图
我使用 go:noinline 指令,阻止在 main 函数中,编译器使用内联代码替代函数调用 。内联(优化)会使函数调用消失,并使例子复杂化 。我将在下一篇博文介绍内联造成的副作用 。
在表 1 中,你可以看到创建 user 值,并返回给调用者的两个不同的函数 。在函数版本 1 中,返回值 。
清单 2
文章插图
我说这个函数返回的是值是因为这个被函数创建的 user 值被拷贝并传递到调用栈上 。这意味着调用函数接收到的是这个值的拷贝 。
你可以看下第 17 行到 20 行 user 值被构造的过程 。然后在第 23 行,user 值的副本被传递到调用栈并返回给调用者 。函数返回后,栈看起来如下所示 。
图 1
文章插图
你可以看到图 1 中,当调用完 createUserV1 ,一个 user 值同时存在(两个函数的)栈帧中 。在函数版本 2 中,返回指针 。
清单 3
文章插图
我说这个函数返回的是指针是因为这个被函数创建的 user 值通过调用栈被共享了 。这意味着调用函数接收到一个值的地址拷贝 。
你可以看到在第 28 行到 31 行使用相同的字段值来构造 user 值,但在第 34 行返回时却是不同的 。不是将 user 值的副本传递到调用栈,而是将 user 值的地址传递到调用栈 。基于此,你也许会认为栈在调用之后是这个样子 。
推荐阅读
- 百度与谷歌搜索机制十大区别
- 前端入门:重学Cookie与Session
- 清新福建·运动之旅“寿宁高山茶杯”山径赛激情开跑
- 任嘉伦搞笑视频 任嘉伦搞笑图片
- 世界三大高香红茶之的大吉岭红茶
- 印度大吉岭红茶之等级划分
- 多肉虹之玉和虹之玉锦 多肉虹之玉锦怎么养
- 人际交往的意义有哪些 人际交往的重要性
- 钱串 钱串怎么砍头繁殖
- 微信被“拉黑”之后浑然不知?别再蒙在鼓里,这些方法能有效辨别