一文带你轻松理解Go中的内存逃逸问题

 更新时间:2023年06月28日 10:06:16   作者:7small7  
这篇文章主要给大家介绍Go中的内存逃逸问题,文中通过代码示例讲解的非常详细,对我们的学习或工作有一定的参考价值,感兴趣的同学可以跟着小编一起来学习

内存逃逸是什么

  • 在程序中,每个函数块都会有自己的内存区域用来存自己的局部变量(内存占用少)、返回地址、返回值之类的数据,这一块内存区域有特定的结构和寻址方式,寻址起来十分迅速,开销很少。这一块内存地址称为栈。
  • 栈是线程级别的,大小在创建的时候已经确定,当变量太大的时候,会"逃逸"到堆上,这种现象称为内存逃逸。
  • 简单来说,局部变量通过堆分配和回收,就叫内存逃逸。

内存逃逸危害

  • 堆是一块没有特定结构,也没有固定大小的内存区域,可以根据需要进行调整。
  • 全局变量,内存占用较大的局部变量,函数调用结束后不能立刻回收的局部变量都会存在堆里面。
  • 变量在堆上的分配和回收都比在栈上开销大的多。
  • 对于 go 这种带 GC 的语言来说,会增加 gc 压力,同时也容易造成内存碎片。

内存逃逸现象

  • 向 channel 发送指针数据。因为在编译时,不知道channel中的数据会被哪个 goroutine 接收,因此编译器没法知道变量什么时候才会被释放,因此只能放入堆中。
  • 局部变量在函数调用结束后还被其他地方使用,比如函数返回局部变量指针或闭包中引用包外的值。因为变量的生命周期可能会超过函数周期,因此只能放入堆中。
  • 在 slice 或 map 中存储指针。比如 []*string,其后面的数组可能是在栈上分配的,但其引用的值还是在堆上。
  • 切片扩容后长度太大,导致栈空间不足,逃逸到堆上。
  • 在 interface 类型上调用方法。 在 interface 类型上调用方法时会把interface变量使用堆分配, 因为方法的真正实现只能在运行时知道。

逃逸分析原则

  • 编译阶段无法确定的参数,会逃逸到堆上;
  • 变量在函数外部存在引用,会逃逸到堆上;不存在引用,则会继续在栈上;
  • 变量占用内存较大时,会逃逸到堆上;

内存逃逸解决

  • 对于小型的数据,使用传值而不是传指针,避免内存逃逸。
  • 避免使用长度不固定的slice切片,在编译期无法确定切片长度,只能将切片使用堆分配。
  • interface调用方法会发生内存逃逸,在热点代码片段,谨慎使用。
    避免内存逃逸需要遵循如下两个原则:
  • 指向栈对象上的指针不能被存储到堆中。
  • 指向栈对象上的指针不能超过该栈对象的声明周期。

具体案例

参数为interface类型会逃逸

下面通过举例,来进一步论证逃逸分析的原则,加深一下理解

我们可以使用这个命令go build -gcflags '-m -m -l' go文件名,来查看逃逸分析的结果。

func main() {
  num := 1
  fmt.Println(num)
}

原因分析:

func Println(a ...interface{}) (n int, err error),这个函数的入参是interface类型,编译阶段无法确定其具体的参数类型,所以内存分配到堆上

变量在函数外部有引用会逃逸

func main() {
  _ = test()
}
func test() *int {
  num := 10
  return &num
}

原因分析:

变量num在函数外部存在引用,函数退出时栈中的内存(栈帧)已经释放,但引用已经被返回,如果通过引用地址取值,在栈中是取不到值的,所以Go为了避免这个情况,会将内存分配到堆上。

变量占用内存较大时会逃逸

func main() {
  //不会逃逸
  s1 := make([]int, 10, 10)
  for i := 0; i < 10; i++ {
    s1[i] = i
  }
  //会逃逸
  s2 := make([]int, 10000, 10000)
  for i := 0; i < 10000; i++ {
    s2[i] = i
  }
}

原因分析:

切片容量过大时,会产生逃逸,内存分配到堆上;容量小时,不会逃逸,内存分配依赖在栈上。

变量大小不确定时会逃逸

func main() {
  num := 10
  s := make([]int, num, num) 
  for i := 0; i < num; i++ {
    s[i] = i
  }
}

原因分析:

切片的长度和容量,虽然通过声明的变量num来指定了,但在编译阶段是未知的,并不确定num的具体值,所以会逃逸,将内存分配到堆上。

到此这篇关于一文带你轻松理解Go中的内存逃逸问题的文章就介绍到这了,更多相关Go 内存逃逸内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 一文带你吃透Go语言中的原子操作

    一文带你吃透Go语言中的原子操作

    原子操作是解决并发编程中共享数据访问问题的一种常见机制,下面就来和大家深入介绍原子操作的原理、用法以及在解决并发问题中的应用,需要的可以参考一下
    2023-06-06
  • Go语言接口的嵌套的具体使用

    Go语言接口的嵌套的具体使用

    在Go语言中,不仅结构体与结构体之间可以嵌套,接口与接口间也可以通过嵌套创造出新的接口,本文主要介绍了Go语言接口的嵌套的具体使用,感兴趣的可以了解一下
    2023-04-04
  • 一文带你了解Golang中interface的设计与实现

    一文带你了解Golang中interface的设计与实现

    本文就来详细说说为什么说 接口本质是一种自定义类型,以及这种自定义类型是如何构建起 go 的 interface 系统的,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-01-01
  • Go引用github包的详细流程步骤

    Go引用github包的详细流程步骤

    这篇文章主要给大家介绍了关于Go引用github包的详细流程步骤,文中通过图文介绍的非常详细,对大家学习或者使用Go具有一定的参考价值,需要的朋友可以参考下
    2024-02-02
  • 浅析Golang中类型嵌入的简介与使用

    浅析Golang中类型嵌入的简介与使用

    类型嵌入指的就是在一个类型的定义中嵌入了其他类型,Go 语言支持两种类型嵌入,分别是接口类型的类型嵌入和结构体类型的类型嵌入,下面我们就来详细一下类型嵌入的使用吧
    2023-11-11
  • 使用go连接clickhouse的实战操作

    使用go连接clickhouse的实战操作

    这篇文章主要给大家介绍了关于使用go连接clickhouse的实战操作,文中通过实例代码介绍的非常详细,对大家学习或者使用go具有一定的参考学习价值,需要的朋友可以参考下
    2023-03-03
  • Golang跳转语句continue与goto使用语法详解

    Golang跳转语句continue与goto使用语法详解

    这篇文章主要介绍了Golang跳转语句continue与goto使用语法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-01-01
  • Go语言实现本地缓存的策略详解

    Go语言实现本地缓存的策略详解

    今天给大家分享的是Go语言本地缓存的一些内容,主要是结合bigcache和fastcache两个优秀的开源代码库,总结一些设计思路和感悟,文章通过代码示例介绍的非常详细,需要的朋友可以参考下
    2023-07-07
  • 一文搞懂Go语言操作Redis的方法

    一文搞懂Go语言操作Redis的方法

    Redis是一个开源的内存数据库,在项目开发中redis的使用也比较频繁,本文介绍了Go语言中go-redis库的基本使用。感兴趣的小伙伴们可以参考借鉴一下
    2022-09-09
  • 详解Go语言中的内存对齐

    详解Go语言中的内存对齐

    前面我们学习了Go语言空结构体详解,最近又在看unsafe包的知识,在查阅相关资料时不免会看到内存对齐相关的内容。虽然不会,但可以学呀,那么这篇文章,我们就一起来看下什么是内存对齐吧
    2022-10-10

最新评论