小结:

1、当形参为 interface 类型时,在编译阶段编译器无法确定其具体的类型。因此会产生逃逸,最终分配到堆上。

2、The construction of a value doesn’t determine where it lives.  Anytime you share a value up the call stack, it is going to escape.

3、

However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors.

Frequently Asked Questions (FAQ) - The Go Programming Language https://golang.org/doc/faq#stack_or_heap

How do I know whether a variable is allocated on the heap or the stack? 

From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

The construction of a value doesn’t determine where it lives. Only how a value is shared will determine what the compiler will do with that value. Anytime you share a value up the call stack, it is going to escape. There are other reasons for a value to escape which you will explore in the next post.

What these posts are trying to lead you to is guidelines for choosing value or pointer semantics for any given type. Each semantic comes with a benefit and cost. Value semantics keep values on the stack which reduces pressure on the GC. However, there are different copies of any given value that must be stored, tracked and maintained. Pointer semantics place values on the heap which can put pressure on the GC. However, they are efficient because there is only one value that needs to be stored, tracked and maintained. The key is using each semantic correctly, consistently and in balance.

golang 逃逸分析与栈、堆分配分析_惜暮-CSDN博客 https://louyuting.blog.csdn.net/article/details/102846449

惜暮 2019-10-31 23:47:47 
 

我们在写 golang 代码时候定义变量,那么一个很常见的问题,申请的变量保存在哪里呢?栈?还是堆?会不会有一些特殊例子?这篇文章我们就来探索下具体的case以及如何做分析。

还是从实际使用场景出发:

Question

package main

type User struct {
ID int64
Name string
Avatar string
} func GetUserInfo() *User {
return &User{
ID: 666666,
Name: "sim lou",
Avatar: "https://www.baidu.com/avatar/666666",
}
} func main() {
u := GetUserInfo()
println(u.Name)
}

这里GetUserInfo 函数里面的 User 对象是存储在函数栈上还是堆上?

什么是堆?什么是栈?

简单说:

  • 堆:一般来讲是人为手动进行管理,手动申请、分配、释放。一般所涉及的内存大小并不定,一般会存放较大的对象。另外其分配相对慢,涉及到的指令动作也相对多
  • 栈:由编译器进行管理,自动申请、分配、释放。一般不会太大,我们常见的函数参数(不同平台允许存放的数量不同),局部变量等等都会存放在栈上

今天我们介绍的 Go 语言,它的堆栈分配是通过 Compiler 进行分析,GC 去管理的,而对其的分析选择动作就是今天探讨的重点

逃逸分析

逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针。

通俗地讲,逃逸分析就是确定一个变量要放堆上还是栈上,规则如下:

  • 是否有在其他地方(非局部)被引用。只要有可能被引用了,那么它一定分配到堆上。否则分配到栈上
  • 即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上

对此你可以理解为,逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为。

在什么阶段确立逃逸

go 在编译阶段确立逃逸,注意并不是在运行时

为什么需要逃逸

其实就是为了尽可能在栈上分配内存,我们可以反过来想,如果变量都分配到堆上了会出现什么事情?例如:

  • 垃圾回收(GC)的压力不断增大
  • 申请、分配、回收内存的系统开销增大(相对于栈)
  • 动态分配产生一定量的内存碎片

其实总的来说,就是频繁申请、分配堆内存是有一定 “代价” 的。会影响应用程序运行的效率,间接影响到整体系统。因此 “按需分配” 最大限度的灵活利用资源,才是正确的治理之道。这就是为什么需要逃逸分析的原因,你觉得呢?

go怎么确定是否逃逸

第一:编译器命令

可以看到详细的逃逸分析过程。而指令集 -gcflags 用于将标识参数传递给 Go 编译器,涉及如下:

  • -m 会打印出逃逸分析的优化策略,实际上最多总共可以用 4 个 -m,但是信息量较大,一般用 1 个就可以了
  • -l 会禁用函数内联,在这里禁用掉 inline 能更好的观察逃逸情况,减少干扰

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

第二:反编译命令查看

$ go tool compile -S main.go

注:可以通过 go tool compile -help 查看所有允许传递给编译器的标识参数

实际案例

1.指针

package main

type User struct {
ID int64
Name string
Avatar string
} func GetUserInfo() *User {
return &User{
ID: 666666,
Name: "sim lou",
Avatar: "https://www.baidu.com/avatar/666666",
}
} func main() {
u := GetUserInfo()
println(u.Name)
}

看编译器命令执行结果:

$go build -gcflags '-m -l' escape_analysis.go
# command-line-arguments
./escape_analysis.go:13:11: &User literal escapes to heap

通过查看分析结果,可得知 &User 逃到了堆里,也就是分配到堆上了。这是不是有问题啊…再看看汇编代码确定一下,如下:

$go tool compile -S escape_analysis.go       

"".GetUserInfo STEXT size=190 args=0x8 locals=0x18
0x0000 00000 (escape_analysis.go:9) TEXT "".GetUserInfo(SB), ABIInternal, $24-8
......
0x002c 00044 (escape_analysis.go:13) CALL runtime.newobject(SB)
......
0x0045 00069 (escape_analysis.go:12) CMPL runtime.writeBarrier(SB), $0
0x004c 00076 (escape_analysis.go:12) JNE 156
0x004e 00078 (escape_analysis.go:12) LEAQ go.string."sim lou"(SB), CX
......
0x0061 00097 (escape_analysis.go:13) CMPL runtime.writeBarrier(SB), $0
0x0068 00104 (escape_analysis.go:13) JNE 132
0x006a 00106 (escape_analysis.go:13) LEAQ go.string."https://www.baidu.com/avatar/666666"(SB), CX
......

执行了 runtime.newobject 方法,也就是确实是分配到了堆上。这是为什么呢?这是因为 GetUserInfo() 返回的是指针对象,引用被返回到了方法之外了。因此编译器会把该对象分配到堆上,而不是栈上。否则方法结束之后,局部变量就被回收了,岂不是翻车。所以最终分配到堆上是理所当然的。

那么所有的指针都在堆上?也不是:

func PrintStr()  {
str := new(string)
*str = "hello world"
} func main() {
PrintStr()
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

看编译器逃逸分析的结果:

$go build -gcflags '-m -l' escape_analysis3.go
# command-line-arguments
./escape_analysis3.go:4:12: PrintStr new(string) does not escape
  • 1
  • 2
  • 3

看,该对象分配到栈上了。很核心的一点就是它有没有被作用域之外所引用,而这里作用域仍然保留在 main 中,因此它没有发生逃逸。

2. 不确定类型

func main()  {
str := new(string)
*str = "hello world"
fmt.Println(*str)
}
  • 1
  • 2
  • 3
  • 4
  • 5

执行命令观察一下,如下:

 $go build -gcflags '-m -l' escape_analysis4.go
# command-line-arguments
./escape_analysis4.go:6:12: main new(string) does not escape
./escape_analysis4.go:8:13: main ... argument does not escape
./escape_analysis4.go:8:14: *str escapes to heap
  • 1
  • 2
  • 3
  • 4
  • 5

通过查看分析结果,可得知 str 变量逃到了堆上,也就是该对象在堆上分配。但上个案例时它还在栈上,我们也就 fmt 输出了它而已。这…到底发生了什么事?

相对案例一,案例二只加了一行代码 fmt.Println(str),问题肯定出在它身上。其原型:
func Println(a ...interface{}) (n int, err error)

通过对其分析,可得知当形参为 interface 类型时,在编译阶段编译器无法确定其具体的类型。因此会产生逃逸,最终分配到堆上。

如果你有兴趣追源码的话,可以看下内部的 reflect.TypeOf(arg).Kind() 语句,其会造成堆逃逸,而表象就是 interface 类型会导致该对象分配到堆上。

总结

  • 静态分配到栈上,性能一定比动态分配到堆上好
  • 底层分配到堆,还是栈。实际上对你来说是透明的,不需要过度关心
  • 每个 Go 版本的逃逸分析都会有所不同(会改变,会优化)
  • 直接通过 go build -gcflags ‘-m -l’ 就可以看到逃逸分析的过程和结果
  • 到处都用指针传递并不一定是最好的,要用对。

Language Mechanics On Escape Analysis https://www.ardanlabs.com/blog/2017/05/language-mechanics-on-escape-analysis.html

逃逸分析与栈、堆分配分析 escape_analysis的更多相关文章

  1. (转)java内存分配分析/栈内存、堆内存

    转自(http://blog.csdn.net/qh_java/article/details/9084091) java内存分配分析/栈内存.堆内存 java内存分配分析 本文将由浅入深详细介绍Ja ...

  2. C#中堆和栈的区别分析

    线程堆栈:简称栈 Stack托管堆: 简称堆 Heap 使用.Net框架开发程序的时候,我们无需关心内存分配问题,因为有GC这个大管家给我们料理一切.如果我们写出如下两段代码: 1 代码段1: 2 3 ...

  3. C#中堆和栈的区别分析(有待更新总结2)

    转载:http://blog.csdn.net/Zevin/article/details/5731965 线程堆栈:简称栈 Stack 托管堆: 简称堆 Heap 使用.Net框架开发程序的时候,我 ...

  4. Java内存分析--栈--堆

    Java内存分析--栈--堆 JVM的内存分析: 1.栈内存 1.连续的存储空间,遵循后进先出的原则. 2.每个线程包含一个栈区,栈区只保存基础数据类型的对象和自定义对象的引用. 3.每个栈中的数据都 ...

  5. 如何限制一个类只在堆上分配和栈上分配(StackOnly HeapOnly)

    [本文链接] http://www.cnblogs.com/hellogiser/p/stackonly-heaponly.html [题目] 如何限制一个类只在堆上分配和栈上分配? [代码]  C+ ...

  6. [转]使用Java Mission Control进行内存分配分析

    jdk7u40自带了一个非常好用的工具,就是Java Mission Control.JRockit Misson Control用户应该会对mission control的很多功能十分熟悉,JRoc ...

  7. 如何让对象只在堆或者栈中分配空间ANDC++禁止一个类被继承

    在开始之前先来分析一下C++中的new运算符和operator new之间的关联. new:指我们在C++里通常用到的运算符,比如A* a = new A或者调用带参数的构造函数;  对于new来说, ...

  8. 栈 堆 stack heap 堆内存 栈内存 内存分配中的堆和栈 掌握堆内存的权柄就是返回的指针 栈是面向线程的而堆是面向进程的。 new/delete and malloc/ free 指针与内存模型

    小结: 1.栈内存 为什么快? Due to this nature, the process of storing and retrieving data from the stack is ver ...

  9. 源码分析:Java堆的创建

    虚拟机在内存中申请一片区域,由虚拟机自动管理,用来满足应用程序对象分配的空间需求,即堆空间. 由于程序运行的局部特性,程序创建的大多数对象都具有非常短的生命周期,而程序也会创建一些生命周期特别长的对象 ...

随机推荐

  1. 安装简易OpenShift单节点并配置DNS泛域名

    1 楔子 公司有个项目部署在 OpenShift 容器云平台上,出现问题较多,于是萌生在公司环境搭建现场仿真环境,由于资源没有生产环境多,就使用单节点简单模拟下 本文主要内容包括以下: 使用 Dnsm ...

  2. easyui中刷新列表

    <table class="crud-content-info" id="showProductDialogFormstandrad"> </ ...

  3. python实现AES/DES/RSA/MD5/SM2/SM4/3DES加密算法模板汇总

    都是作者累积的,且看其珍惜,大家可以尽量可以保存一下,如果转载请写好出处https://www.cnblogs.com/pythonywy 一.md5加密 1.简介 这是一种使用非常广泛的加密方式,不 ...

  4. 为什么import React from 'react',React首字母必须大写?

    很奇怪的是,明明没有用到 React,但是我不得不 import React.这是为什么? import React from 'react'; export default function (pr ...

  5. 破解版IDM使用问题

    正版的IDM一般下载安装后有30天的免费使用期,过了就需要买正版序列号才能使用,网上一般提供的破解版的IDM安装后又存在无法添加到chrome插件的问题 这里针对这个问题给出解决方案: 首先下载破解版 ...

  6. SQL Server中模式(schema)、数据库(database)、表(table)、用户(user)之间的关系

    数据库的初学者往往会对关系型数据库模式(schema).数据库(database).表(table).用户(user)之间感到迷惘,总感觉他们的关系千丝万缕,但又不知道他们的联系和区别在哪里,对一些问 ...

  7. reactor模式:主从式reactor

    前面两篇文章提到 reactor模式:单线程的reactor模式 reactor模式:多线程的reactor模式 NIO的server模式只有5个阶段,但是,NIO的selectionkey里确实有个 ...

  8. windows中关闭端口的方法

    打开cmd:输入-ano | findstr "端口号" 控制台会输出占用端口的pid 如"8380" 再向cmd中输入 taskkill /f/pid 838 ...

  9. Java中几种常见的循环

    多重if_else: package com.dengchaoqun.ht; public class Double_For02 { /** * * 打印乘法表 */ public static vo ...

  10. 浅入kubernetes(2):Kubernetes 的组成

    目录 说明 Kubernetes集群的组成 What are containerized applications? What are Kubernetes containers? What are ...