Go变量逃逸分析
写过C/C++的同学都知道,调用著名的malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程序员。一不小心,就会发生内存泄露,搞得胆战心惊。
切换到Golang后,基本不会担心内存泄露了。虽然也有new函数,但是使用new函数得到的内存不一定就在堆上。堆和栈的区别对程序员“模糊化”了,当然这一切都是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,讲得非常好!
C++是公认的语法最复杂的语言,据说没有人可以完全掌握C++的语法。而这一切在Go语言中就大不相同了。像上面示例的C++代码放到Go里,没有任何问题。
你表面的光鲜,一定是背后有很多人为你撑起的!Go语言里就是编译器的逃逸分析。它是编译器执行静态代码分析后,对内存管理进行的优化和简化。
在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。
更简单来说,逃逸分析决定一个变量是分配在堆上还是分配在栈上。
为什么要逃逸分析
前面讲的C/C++中出现的问题,在Go中作为一个语言特性被大力推崇。真是C/C++之砒霜Go之蜜糖!
C/C++中动态分配的内存需要我们手动释放,导致猿们平时在写程序时,如履薄冰。这样做有他的好处:程序员可以完全掌控内存。但是缺点也是很多的:经常出现忘记释放内存,导致内存泄露。所以,很多现代语言都加上了垃圾回收机制。
Go的垃圾回收,让堆和栈对程序员保持透明。真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。把那些内存管理的复杂机制交给编译器,而程序员可以去享受生活。
逃逸分析这种“骚操作”把变量合理地分配到它该去的地方,“找准自己的位置”。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。真正地做到“按需分配”,提前实现共产主义!
如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销(占用CPU容量的25%)。
堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。
通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。
逃逸分析是怎么完成的
Go逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。
简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。
Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。
对一个变量取地址,可能会被分配到堆上。但是编译器进行逃逸分析后,如果考察到在函数返回后,此变量不会被引用,那么还是会被分配到栈上。套个取址符,就想骗补助?Too young!
简单来说,编译器会根据变量是否被外部引用来决定是否逃逸:
- 如果函数外部没有引用,则优先放到栈中;
- 如果函数外部存在引用,则必定放到堆中;
针对第一条,可能放到堆上的情形:定义了一个很大的数组,需要申请的内存过大,超过了栈的存储能力。
逃逸分析实例
Go提供了相关的命令,可以查看变量是否发生逃逸。
还是用上面我们提到的例子:
package main
import "fmt"
func foo() *int {
t := 3
return &t;
}
func main() {
x := foo()
fmt.Println(*x)
}
foo函数返回一个局部变量的指针,main函数里变量x接收它。执行如下命令:
go build -gcflags '-m -l' main.go
加-l是为了不让foo函数被内联。得到如下输出:
# command-line-arguments
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{}),编译期间很难确定其参数的具体类型,也会发生逃逸。
使用反汇编命令也可以看出变量是否发生逃逸。
go tool compile -S main.go
截取部分结果,图中标记出来的说明t是在堆上分配内存,发生了逃逸。

总结
堆上动态分配内存比栈上静态分配内存,开销大很多。
变量分配在栈上需要能在编译期确定它的作用域,否则会分配到堆上。
Go编译器会在编译期对考察变量的作用域,并作一系列检查,如果它的作用域在运行期间对编译器一直是可知的,那么就会分配到栈上。
简单来说,编译器会根据变量是否被外部引用来决定是否逃逸。对于Go程序员来说,编译器的这些逃逸分析规则不需要掌握,我们只需通过go build -gcflags '-m'命令来观察变量逃逸情况就行了。
不要盲目使用变量的指针作为函数参数,虽然它会减少复制操作。但其实当参数为变量自身的时候,复制是在栈上完成的操作,开销远比变量逃逸后动态地在堆上分配内存少的多。
最后,尽量写出少一些逃逸的代码,提升程序的运行效率。
转载自:
Golang之变量去哪儿?
Go变量逃逸分析的更多相关文章
- JVM中启用逃逸分析
-XX:+DoEscapeAnalysis 逃逸分析优化JVM原理我们知道java对象是在堆里分配的,在调用栈中,只保存了对象的指针.当对象不再使用后,需要依靠GC来遍历引用树并回收内存,如果对象数量 ...
- JVM笔记-逃逸分析
参考: http://www.iteye.com/topic/473355http://blog.sina.com.cn/s/blog_4b6047bc01000avq.html 什么是逃逸分析(Es ...
- JVM逃逸分析
开启逃逸分析: -server -XX:+DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m 关闭逃逸分析: -server -XX:-DoEsca ...
- 逃逸分析(Escape Analysis)
一.什么是逃逸 逃逸是指在某个方法之内创建的对象,除了在方法体之内被引用之外,还在方法体之外被其它变量引用到:这样带来的后果是在该方法执行完毕之后,该方法中创建的对象将无法被GC回收,由于其被其它变量 ...
- golang逃逸分析和竞争检测
最近在线上发现一块代码逻辑在执行N次耗时波动很大1ms~800ms,最开始以为是gc的问题,对代码进行逃逸分析,看哪些变量被分配到堆上了,后来发现是并发编程时对一个切片并发的写,导致存在竞争,类似下面 ...
- 深入理解Java中的逃逸分析
在Java的编译体系中,一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经过两段编译,第一段是把.java文件转换成.class文件.第二段编译是把.class转换成机器指令的过程. ...
- JVM的逃逸分析
我们都知道Java中的对象默认都是分配到堆上,在调用栈中,只保存了对象的指针.当对象不再使用后,需要依靠GC来遍历引用树并回收内存.如果堆中对象数量太多,回收对象还有整理内存,都会会带来时间上的消耗, ...
- Java之JVM逃逸分析
引言: 逃逸分析(Escape Analysis)是众多JVM技术中的一个使用不多的技术点,本文将通过一个实例来分析其使用场景. 概念 逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配 ...
- java虚拟机的逃逸分析
逃逸分析作为其他优化手段提供依据的分析技术,其基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸.甚至还有可能被外部线程 ...
随机推荐
- SQL优化(面试题)
因为现在面试经常需要问的需要SQL优化,问的具体操作步骤时候的常见做法,所以网上总结这些操作步骤: SQL优化的具体操作: 1.在表中建立索引,优先考虑where.group by使用到的字段. 2. ...
- Docker commit 命令保存的镜像文件太大的问题
基础镜像: centos7.5 进入容器后, 先后安装了 jdk1.8, maven3.6.0, git, rocketmq4.3.2 安装完成后使用 docker commit 命令保存为镜像 结果 ...
- 关于vue的computed、filters、watch
filters 这个属性大家可能用的不是很多 因为一般的数组过滤我们用 es6的filter就能完成了 我想到一个场景,网上买书促销 满100减50 满两百减100 <input type=&q ...
- keras安装-【老鱼学keras】
为何要用keras? 两个字:简单. Keras让深度学习像搭建积木一样方便地来进行,使前面的tensorflow能够更加方便地使用. 虽然还有其它更多的理由,比如:Keras 支持多个后端引擎,不会 ...
- 学习使人快乐8--Maven
一.maven基操: MAVEN依赖之 坐标: 二.MAVEN依赖 type:依赖的类型,比如是jar包还是war包等 默认为jar,表示依赖的jar包 optional:标记依赖是否可选.默认值fa ...
- docker 安装 hbase安装 java连接hbase (mac环境)
docker 安装 https://hub.docker.com/editions/community/docker-ce-desktop-mac 下载地址 下载完之后,安装app一样安装就好 安装完 ...
- python接口自动化1
组织架构: 包括配置文件,反射.文件路径.Excel操作.测试报告生成 case.config [MODE] file_name=case_data.xlsx mode={"register ...
- 安装VUE Cli3 框架方法
下面为大家介绍一下怎样安装 VUE Cli3的步骤 官网地址 https://cli.vuejs.org/zh/guide/installation.html 一.首先要检查一下是否安装node环 ...
- java位移运算符2 转
https://blog.csdn.net/xxx134617/article/details/7454774 java中int类型占4个字节,二进制用补码表示: 3的二进制表示: 00000000 ...
- FW/IDS/IPS/WAF等安全设备部署方式及优缺点
现在市场上的主流网络安全产品可以分为以下几个大类:1.基础防火墙FW/NGFW类 主要是可实现基本包过滤策略的防火墙,这类是有硬件处理.软件处理等,其主要功能实现是限制对IP:port的访问.基本上的 ...