Go语言中的逃逸分析究竟是什么?

目录
  • 1、逃逸分析介绍
  • 2、Go中内存分配在哪里?
  • 3、Go与C++内存分配的区别
  • 4、逃逸分析骚操作
  • 5、逃逸分析引申示例说明

1、逃逸分析介绍

学计算机的同学都知道,在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了“逃逸”。

Go语言的逃逸分析是编译器执行静态代码分析后,对内存管理进行的优化和简化,它可以决定一个变量是分配到堆还栈上。

写过C/C++的小伙伴应该知道,使用比较经典的mallocnew函数可以在堆上分配一块内存,这块内存的使用和回收(销毁)的任务在程序员中,处理不当,很可能会发生内存泄露。

2、Go中内存分配在哪里?

但是在Go语言中,基本不用担心内存泄露的问题,因为内存回收Go语言中已经帮我们处理了(GC回收机制)。虽然也有new函数,但是使用new函数得到的内存不一定就在堆上。堆和栈的区别对程序员“模糊化”了,当然这一切都是Go编译器在背后帮我们完成的。

Go语言逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。

简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。

Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。

对一个变量取地址,可能会被分配到堆上。但是编译器进行逃逸分析后,如果考察到在函数返回后,此变量不会被引用,那么还是会被分配到栈上。

编译器会根据变量是否被外部引用来决定是否逃逸:

  • 如果在函数外面没有引用到,则优先放到栈区中;
  • 如果在函数外面存在引用的可能,则就会放到堆区中;

当我们写C/C++代码时,为了提高效率,会经常将pass-by-value(传值)提升成pass-by-reference,企图避免构造函数的运行,并且直接返回一个指针。

你一定还记得,这里隐藏了一个很大的坑:在函数内部定义了一个局部变量,然后返回这个局部变量的地址(指针)。这些局部变量是在栈上分配的(静态内存分配),一旦函数执行完毕,变量占据的内存会被销毁,任何对这个返回值作的动作(如解引用),都将扰乱程序的运行,甚至导致程序直接崩溃。比如下面的这段代码:

int *foo ( void )
{
    int t = 3;
    return &t;
}

有些同学可能知道上面这个坑,用了个更聪明的做法:在函数内部使用new函数构造一个变量(动态内存分配),然后返回此变量的地址。因为变量是在堆上创建的,所以函数退出时不会被销毁。

但是,这样就行了吗?new出来的对象该在何时何地delete呢?调用者可能会忘记delete或者直接拿返回值传给其他函数,之后就再也不能delete它了,也就是发生了内存泄露。关于这个坑,大家可以去看看《Effective C++》条款21,讲得非常好!

3、Go与C++内存分配的区别

上面讲的C/C++中会遇到的问题,在Go中作为一个语言特性被大力推崇,可以解决以上的难点!

C/C++中的动态分配的内存需要我们手动来释放,这样会带来一个问题:有些内存处理不当或回收不及时,导致内存泄露。

但是这样的好处是:开发人员可以自己管理内存。

Go的垃圾回收,让堆和栈对程序员保持透明。真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。把那些内存管理的复杂机制交给编译器,而程序员可以去享受生活。

4、逃逸分析骚操作

逃逸分析这种“骚操作”把变量合理地分配到它该去的地方。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。

如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销(占用CPU容量的25%)。

堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。

通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。

5、逃逸分析引申示例说明

引申1:如何查看某个变量是否发生了逃逸?两种方法:使用go命令,查看逃逸分析结果;反汇编源码;

比如用这个例子:

package main
import "fmt"
func foo() *int {
    t := 3
    return &t;
}
func main() {
    x := foo()
    fmt.Println(*x)
}

使用go命令:

go build -gcflags '-m -l' main.go

加-l是为了不让foo函数被内联。得到如下输出:

# 命令行变量
src/main.go:7:9: &t escapes to heap
src/main.go:6:7: moved to heap: t
src/main.go:12:14: *x escapes to heap
src/main.go:12:13: main ... argument does not escape

foo函数里的变量t逃逸了,和我们预想的一致。让我们不解的是为什么main函数里的x也逃逸了?这是因为有些函数参数为interface类型,比如fmt.Println(a …interface{}) ,编译期间很难确定其参数的具体类型,也会发生逃逸。

反汇编代码比较难理解,这里就不讲了。

引申2:下面代码中的变量发生逃逸了吗?

先来看示例1:

package main
type S struct {}
func main() {
  var x S
  _ = identity(x)
}
func identity(x S) S {
  return x
}

分析:Go语言函数传递都是通过值的,调用函数的时候,直接在栈上copy出一份参数,不存在逃逸。

 再来看示例二:

package main
type S struct {}
func main() {
  var x S
  y := &x
  _ = *identity(y)
}
func identity(z *S) *S {
  return z
}

分析:identity函数的输入直接当成返回值了,因为没有对z作引用,所以z没有逃逸。对x的引用也没有逃出main函数的作用域,因此x也没有发生逃逸。

 继续看示例三:

package main
type S struct {}
func main() {
  var x S
  _ = *ref(x)
}
func ref(z S) *S {
  return &z
}

分析:z是对x的拷贝,ref函数中对z取了引用,所以z不能放在栈上,否则在ref函数之外,通过引用如何找到z,所以z必须要逃逸到堆上。仅管在main函数中,直接丢弃了ref的结果,但是Go的编译器还没有那么智能,分析不出来这种情况。而对x从来就没有取引用,所以x不会发生逃逸。

还有示例四:如果对一个结构体成员赋引用如何?

package main
type S struct {
  M *int
}
func main() {
  var i int
  refStruct(i)
}
func refStruct(y int) (z S) {
  z.M = &y
  return z
}

分析:refStruct函数对y取了引用,所以y发生了逃逸。

 最后看示例五:

package main
type S struct {
  M *int
}
func main() {
  var i int
  refStruct(&i)
}
func refStruct(y *int) (z S) {
  z.M = y
  return z
}

分析:main函数里对i取了引用,并且把它传给了refStruct函数,i的引用一直在main函数的作用域用,因此i没有发生逃逸。和上一个例子相比,有一点小差别,但是导致的程序效果是不同的:例子4中,i先在main的栈帧中分配,之后又在refStruct栈帧中分配,然后又逃逸到堆上,到堆上分配了一次,共3次分配。本例中,i只分配了一次,然后通过引用传递。

到此这篇关于Go语言中的逃逸分析究竟是什么?的文章就介绍到这了,更多相关Go语言中的逃逸内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go语言yaml转map、map遍历的实现

    yaml文件内容 apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: mysql-snapshot spec: privileged: false allowPrivilegeEscalation: false volumes: - "*" hostNetwork: false hostIPC: false hostPID: false runAsUser: rule: RunAsAny seLinux:

  • Go语言使用defer+recover解决panic导致程序崩溃的问题

    案例:如果我们起了一个协程,但这个协程出现了panic,但我们没有捕获这个协程,就会造成程序的崩溃,这时可以在goroutine中使用recover来捕获panic,进行处理,这样主线程不会受到影响. 代码如下: package main import ( "fmt" "time" ) func sayHello() { for i := 0; i < 10; i++ { time.Sleep(time.Second) fmt.Println("he

  • GO语言入门Golang进入HelloWorld

    目录 概述 Go 语言结构 新建项目 Hello World 概述 Golang 是一个跨平台的新生编程语言. 今天小白就带大家一起携手走进 Golang 的世界. (第 1 课) Go 语言结构 在我们开始学习 Go编程语言的基础构建模块前, 我们吸纳来了解一下 Go 语言的程序结构. Go 语言的基础组成有以下几个部分: 包声明引入包函数变量语句 & 表达式注释 新建项目 Hello World 代码: package main // 属于可执行的文件 import "fmt&quo

  • Go语言安装和GoLand2021最全超详细安装教程

    目录 前述 一.Go语言的安装和下载. 二.GoLand2021安装和下载 参考教程 前述 Go语言和GoLand的关系好比于java和idea.python和pycharm,因此我们需要先安装好Go语言后才能安装GoLand.它的安装和java,python的安装大同小异. 一.Go语言的安装和下载. Go语言下载地址:Go语言 选择相对应的安装包进行下载,由于电脑是windows系统,下载的是go1.17.windows-amd64.msi,记住要下载后缀为msi的文件. 下载后得到以下文件

  • Go语言使用钉钉机器人推送消息的实现示例

    学习了Go语言后,打算利用最近比较空一点,写一个前端部署工具,不需要每次都复制粘贴的麻烦,我们希望再部署开始之前和部署结束后推送钉钉消息 创建一个钉钉机器人 这个比较简单 添加完后会给你一个webhook就是我们发送消息的地址 推送消息 show code! func SendDingMsg(msg string) { //请求地址模板 webHook := `https://oapi.dingtalk.com/robot/send?access_token=04c381fc31944ad290

  • Go语言基础语法之结构体及方法详解

    结构体类型可以用来保存不同类型的数据,也可以通过方法的形式来声明它的行为.本文将介绍go语言中的结构体和方法,以及"继承"的实现方法. 结构体类型 结构体类型(struct)在go语言中具有重要地位,它是实现go语言面向对象编程的重要工具.go语言中没有类的概念,可以使用结构体实现类似的功能,传统的OOP(Object-Oriented Programming)思想中的继承在go中可以通过嵌入字段的方式实现. 结构体的声明与定义: // 使用关键字 type 和 struct 定义名字

  • go语言实现mqtt协议的实践

    目录 一.什么是MQTT 二.Go语言MQTT服务器Broker的搭建 三.Go客户端访问简单API 一.什么是MQTT MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(publish/subscribe)模式的"轻量级"通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布. MQTT最大优点在于,可以以极少的代码和有限的带宽,为连接远程设备提供实时可靠的消息服务.作为一种低开销.低带宽占用的

  • Go语言中CGO的使用实践

    目录 1. Go语言调用C函数例子: 2. Go语言调用C库函数: 3. Go语言导出函数给C语言使用: 4. Go语言导出函数指针给c语言使用: 部门产品业务功能采用Golang开发,但是有些功能是用c写的,比如说net-snmp,bfd协议等等,像这些如果使用GO语言重编的话,既有实现的复杂度也需要相当长的时间,好在GO语言提供了CGO机制,使得能够在go代码中直接调用C的库函数,大大提高了效率,减少了重复开发工作,此外还支持在C语言中调用GO函数,这一点还是蛮强大的. 1. Go语言调用C

  • Go语言变量与基础数据类型详情

    目录 一.基础介绍 1.Go 的特性 2.Go 的常用命令 3.Hello Word 二.变量 1.定义方式 三.常量 四.基础数据类型 1.数字 2.浮点型(小数,32/64表示小数点后长度多少位) 3.布尔(Bool) 4.字符串 一.基础介绍 Go 是静态(编译型)语言,是区别于解释型语言的弱类型语言(静态:类型固定,强类型:不同类型不允许直接运算) 例如 python 就是动态强类型语言 1.Go 的特性 跨平台的编译型语言,交叉编译 管道(channel),切片(slice),并发(r

  • Go语言中的逃逸分析究竟是什么?

    目录 1.逃逸分析介绍 2.Go中内存分配在哪里? 3.Go与C++内存分配的区别 4.逃逸分析骚操作 5.逃逸分析引申示例说明 1.逃逸分析介绍 学计算机的同学都知道,在编译原理中,分析指针动态范围的方法称之为逃逸分析.通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了"逃逸". Go语言的逃逸分析是编译器执行静态代码分析后,对内存管理进行的优化和简化,它可以决定一个变量是分配到堆还栈上. 写过C/C++的小伙伴应该知道,使用比较经典的malloc和new函数可以

  • 简述Java编程语言中的逃逸分析

    大家一般认为new出来的对象都是被分配在堆上,但这并不是完全正确,通过对Java对象分配过程分析,我们发现对象除了可以被分配在堆上,还可以在栈或TLAB中分配空间.而栈上分配对象的技术基础是逃逸分析和标量替换,本文主要介绍下逃逸分析. 1.逃逸分析的定义 逃逸分析:是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法. 通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上. Java在Java SE

  • 深入了解java中的逃逸分析

    逃逸分析 public static StringBuffer craeteStringBuffer(String s1, String s2) { StringBuffer sb = new StringBuffer(); sb.append(s1); sb.append(s2); return sb; } public static String createStringBuffer(String s1, String s2) { StringBuffer sb = new StringBu

  • 面试中遇到的java逃逸分析问题

    前言 记得几年前有一次栈长去面试,问到了这么一个问题: Java中的对象都是在堆中分配吗?说明为什么! 当时我被问得一脸蒙逼,瞬间被秒杀得体无完肤,当时我压根就不知道他在考什么知识点,难道对象不是在堆中分配吗?最后就没然后了,回去等通知了.. 下面我收集了一下网友的回答. 回答很精彩,大家可以加入一起搞技术,我现在将答案总结一下给大家. 什么是逃逸分析? 关于 Java 逃逸分析的定义: 逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的

  • Java逃逸分析详解及代码示例

    概念引入 我们都知道,Java 创建的对象都是被分配到堆内存上,但是事实并不是这么绝对,通过对Java对象分配的过程分析,可以知道有两个地方会导致Java中创建出来的对象并一定分别在所认为的堆上.这两个点分别是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)线程私有的缓存区. 基本概念介绍 逃逸分析,是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法.通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的

  • Go逃逸分析示例详解

    目录 引言大纲 逃逸分析 内存管理 栈 堆 堆和栈的对比 加锁 性能 缓存策略 逃逸分析优势 逃逸分析原则 逃逸分析举例 1.参数是interface类型 2. 变量在函数外部有引用 3. 变量内存占用较大 4. 变量大小不确定时 思考题 总结 引言大纲 这个月我会整理分享一系列后端工程师求职面试相关的文章,知识脉络图如下: JAVA/GO/PHP 面试常问的知识点 DB:MySql PgSql Cache: Redis MemCache MongoDB 数据结构 算法 微服务&高并发 流媒体

  • Go语言中的指针运算实例分析

    本文实例分析了Go语言中的指针运算方法.分享给大家供大家参考.具体分析如下: Go语言的语法上是不支持指针运算的,所有指针都在可控的一个范围内使用,没有C语言的*void然后随意转换指针类型这样的东西.最近在思考Go如何操作共享内存,共享内存就需要把指针转成不同类型或者对指针进行运算再获取数据. 这里对Go语言内置的unsafe模块做了一个实验,发现通过unsafe模块,Go语言一样可以做指针运算,只是比C的方式繁琐一些,但是理解上是一样的. 下面是实验代码: 复制代码 代码如下: packag

  • Go语言中普通函数与方法的区别分析

    本文实例分析了Go语言中普通函数与方法的区别.分享给大家供大家参考.具体分析如下: 1.对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然. 2.对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以. 以下为简单示例: 复制代码 代码如下: package structTest    //普通函数与方法的区别(在接收者分别为值类型和指针类型的时候)  //Date:2014-4-3 10:00:07    import ( 

  • Go语言中错误处理实例分析

    本文实例讲述了Go语言中错误处理的方法.分享给大家供大家参考.具体分析如下: 错误是可以用字符串描述自己的任何东西. 主要思路是由预定义的内建接口类型 error,和其返回返回字符串窜的方法 Error 构成. type error interface { Error() string } 当用 fmt 包的多种不同的打印函数输出一个 error 时,会自动的调用该方法. 复制代码 代码如下: package main import (     "fmt"     "time

  • Go语言中的方法定义用法分析

    本文实例讲述了Go语言中的方法定义.分享给大家供大家参考.具体分析如下: 事实上,可以对包中的任意类型定义任意方法,而不仅仅是结构体. 不能对来自其他包的类型或基础类型定义方法. 复制代码 代码如下: package main import (     "fmt"     "math" ) type MyFloat float64 func (f MyFloat) Abs() float64 {     if f < 0 {         return fl

随机推荐