Go从入门到精通——变量声明周期——变量能够使用的代码范围(堆、栈和变量逃逸)

变量能够使用的代码范围(堆、栈和变量逃逸)

  讨论变量声明周期之前,先来了解下计算机组成里两个非常重要的概念:堆和栈。

1、什么是栈?

  栈(stack)是一种拥有特殊规则的线性表数据结构。

  1.1、概念

  栈只允许往线性表的一端放入数据,之后在这一端取出数据,按照后进先出(LIFO,Last In First Out)的顺序。

  往栈中放入元素的过程叫入栈。入栈会增加栈的元素数量,最后放入的元素总是位于栈的顶部,最先放入的元素总是位于栈的底部。

  从栈中取出元素时,只能从栈顶部取出。取出元素后,栈的数量会变少。最先放入的元素总是最后被取出,最后放入的元素总是最先被取出。不允许从栈底获取数据,也不允许对栈成员(除栈顶外的成员)进行任何查看和修改操作。

  1.2、变量和栈有什么关系?

  栈可用于内存分配,栈的分配和回收速度非常快。Go 语言默认情况下会将 变量/函数 分配在栈上,当退出或者结束时,出栈会释放内存,整个分配内存的过程通过栈的分配和回收都会非常的快。

2、什么是堆?

  堆在内存分配中类似往一个房间里摆放各种家具,家具的尺寸有大有小。分配内存时,需要找一块足够装下家具的空间再摆放家具。经过反复摆放和腾空家具后,房间里的空间会变得乱七八糟,此时再往空间里摆放家具会存在虽然有足够的空间,但各空间分布在不同的区域,无法有一段连续的空间来摆放家具的问题。此时内存分配器就需要对这些空间进行调整优化。

  堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。

3、变量逃逸(Escape Analysis)——自动决定变量分配方式,提高运行效率

  堆和栈都有各自优缺点,该怎么在编程中处理这个问题呢?

  在 C/C++ 语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。比如,函数局部变量尽量使用栈,全局变量、结构体成员使用堆内存分配等。程序员不得不花费很多时间在不同的项目中学习、记忆这些概念并加以实践和使用。

  Go 语言将这个过程整合到编译器中,命名为 "变量逃逸分析"。这个技术由编译器分析代码的特征和代码生命周期,决定应该是堆还是使用栈进行内存分配,即 Go 程序员使用了 Go 语言完成了整个工程也不会感受到这个过程。

  Go 程序变量会携带有一组校验数据,用来证明它的整个生命周期是否在运行时完全可知。如果变量通过了这些校验,它就可以在栈上分配。否则就说它 "逃逸" 了,必须在堆上分配

能引起变量逃逸到堆上的典型情况:

  • 在方法内把局部变量指针返回。局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
  • 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
  • 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
  • slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
  • 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。

编译器觉得变量应该分配在堆和栈的原则是:

  • 变量是否被取地址。
  • 变量是否发生逃逸。

  3.1、逃逸分析

package main

import (
    "fmt"
)

//本函数测试入口参数和返回值情况
func dummy(b int) int {

    //声明一个 c 赋值进入参数并返回
    var c int  
    c = b   //C = 0
    return c
}

//空函数,什么也不做
func void(){

}

func main(){
    //声明 a 变量并打印
    var a int

    //调用 void() 函数
    void()

    //打印 a 变量的值和 dummy()函数返回
    fmt.Println(a,dummy(0))   // 这里的a值应该是 int 的零值,等于0。
}

  代码说明如下:

  • 第 8 行,dummy()函数需要传入一个int参数,返回一个int返回值,测试函数参数和返回值分析情况。
  • 第 11 行,声明 c 变量,这里演示函数临时变量通过函数返回值返回后的情况。
  • 第 16 行,这是一个空函数,测试没有任何参数函数的分析情况。
  • 第 23 行,在 main() 中声明 a 变量,测试 main() 中变量的分析情况。
  • 第 26 行,调用 void() 函数,没有返回值,测试 void() 调用后的分析情况。
  • 第 29 行,打印 a 和 dummy(0)的返回值,测试函数返回值没有变量接收时的分析情况。

  运行命令:go run -gcflags "-m -l" 逃逸分析.go

  代码输出,如下图:

 

  程序运行结果分析如下:

  • 通过 ".\逃逸分析.go:29:16: ... argument does not escape" => 这句提示是默认的,可以忽略。
  • 通过 ".\逃逸分析.go:29:16: a escapes to heap" => 代码输出,可以得到 "a 变量没有发生逃逸,只在方法存在时存在,方法结束时会被收回。"。查看源代码第 29 行:fmt.Println(a,dummy(0)) 和 第 23 行 var a int ,可确认是这样的。
  • 通过 ".\逃逸分析.go:29:24: dummy(0) escapes to heap" => 代码输出,可以得到 "dummy(0)的调用逃逸到堆"。查看源代码第 29 行:fmt.Println(a,dummy(0)),由于 dummy() 函数会返回一个整型值,这个值被 fmt.Println 使用后还是会在其声明后继续在 main() 函数中存在。 

   3.2、取地址发生逃逸

  举个例子,使用结构体做数据,了解在堆上分配的情况,代码如下:

package main 

import (
    "fmt"
)

// 声明空结构体测试结构体逃逸情况
type Data struct {
}


func dummy() *Data {

	// 实例化 C 为 Data 类型
	var c Data
	return &c
}

func main(){

	fmt.Println(dummy())
}

  代码说明如下:

  • 第 7 行,声明了一个空结构体做结构体逃逸分析。
  • 第 12 行,将 dummy() 函数的返回值修改为 *Data 指针类型。
  • 第 16 行,取函数局部变量 c 的地址并返回。Go 语言的特性允许这样做。
  • 第 21 行,打印 dummy() 函数的返回值。

  执行逃逸分析:

  • 通过 ".\取地址发生逃逸.go:15:6: moved to heap: c" => 可以得到 "将 c 移动堆中",通过源码第 15 行 "var c Data",Data 是指针类型,可以得出发生变量逃逸,Go 语言最终选择将 c 的 Data 结构分配在堆上,然后由垃圾回收期去回收 c 的内存。
  • 通过 ".\取地址发生逃逸.go:21:13: ... argument does not escape" = > 这句提示是默认的,可以忽略。 

推荐这些文章:

Go语言从入门到精通

1-开发环境
2-go基础
3-流程控制
4-函数
5-面向对象
1-面向对象之-结构体
2-面向对象之-方法
3-面向对象之-接口
4-自定义errors
6-网络编程
1-网络编程之-互联网协议
2-网络编程之-Socket
3-网络编程之-Http
4-网络编程之-WebSocket
7-并发编程
1-并发编程之-并发介绍
2-并...

Go从入门到精通——指针

指针
  指针概念在 Go 语言中被拆分成为两个核心概念:

类型指针:允许对这个指针类型的数据进行修改。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。
切片:由指向起始元素的原始指针、元素数量和容量组成。

C/C++ 中指针饱受诟病的根本原因是指针运算和内存释放。
C/C++ 语言中的裸指针...

Go从入门到精通——函数(function)—— 声明函数

函数(function)—— 声明函数
  函数是组织好的,可重复使用的,用来实现单一或相关功能的代码段,其可以提高应用的模块性和代码的重复利用率。
  Go 语言支持普通函数、函数匿名和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。
  Go 语言的函数属于 “一等公民” (first-class),也就是说:

...

《干就对了。Java从入门到精通》_01入门基础

Java背景知识
  
 
  
 
   
 
   
 
 
             
 
         
 
    
 
 
 
 
 
   
&nbs...

2、golang入门-变量和常量

一、变量
1. 变量的声明
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。
全局变量是允许声明但不使用,局部变量声明不使用会报错
var name string
var age int
var isOk bool
// 批量声明
var (
a string
b in...

Elasticsearch从入门到精通- Elasticsearch 分片分配

Elasticsearch 提供无缝扩展体验的能力的核心在于其跨机器分配工作负载的能力。这是通过sharding. 创建索引时,您为该索引设置主分片和副本分片计数。Elasticsearch 将您的数据和请求分布在这些分片之间,以及跨数据节点的分片上。
集群的容量和性能主要取决于 Elasticsearch 如何在节...

C-(函数)内存介绍

 

...

文章标题:Go从入门到精通——变量声明周期——变量能够使用的代码范围(堆、栈和变量逃逸)
文章链接:https://www.dianjilingqu.com/51218.html
本文章来源于网络,版权归原作者所有,如果本站文章侵犯了您的权益,请联系我们删除,联系邮箱:saisai#email.cn,感谢支持理解。
THE END
< <上一篇
下一篇>>