【Go】堆和栈变量

点击阅读更多查看文章内容

在 Go 语言中,变量的分配位置(堆或栈)是由 编译器 根据变量的生命周期和使用方式自动决定的。以下是堆栈分配的基本规则和常见情况:

1. 栈分配的变量

栈分配的变量通常具有 确定且较短的生命周期,例如局部变量。栈分配速度快,但空间有限。

栈分配的典型情况:

  • 局部变量:

    • 在函数内部声明的变量,且未逃逸到函数外部。

    • 示例:

      1
      2
      3
      4
      func foo() {
      x := 10 // x 分配在栈上
      fmt.Println(x)
      }
  • 函数参数:

    • 传递给函数的参数变量。

    • 示例:

      1
      2
      3
      func bar(y int) {
      fmt.Println(y) // y 分配在栈上
      }

2. 堆分配的变量

堆分配的变量通常具有 不确定或较长的生命周期,例如逃逸到函数外部的变量或动态分配的内存。堆分配速度较慢,但空间较大。

堆分配的典型情况:

  • 全局变量

  • 动态分配的内存:使用 newmake 分配的内存通常分配到堆上。

  • 逃逸到函数外部的变量:

    • 如果变量的地址被返回或传递到函数外部,编译器会将其分配到堆上。

    • 示例:

      1
      2
      3
      4
      func escape() *int {
      x := 10 // x 逃逸到堆上
      return &x
      }
  • 大对象:

    • 较大的对象(如大数组或结构体)可能会分配到堆上,以避免栈溢出。

    • 示例:

      1
      2
      3
      4
      func large() {
      var arr [100000]int // arr 可能分配到堆上
      fmt.Println(arr[0])
      }
  • 闭包捕获的变量:

    • 被闭包捕获的变量会分配到堆上,因为闭包可能在函数返回后继续使用这些变量。

    • 示例:

      1
      2
      3
      4
      5
      6
      func closure() func() int {
      x := 10 // x 逃逸到堆上
      return func() int {
      return x
      }
      }

3. 逃逸分析(Escape Analysis)

Go 编译器通过 逃逸分析 决定变量是否分配到堆上。逃逸分析的目标是尽可能将变量分配到栈上,以提高性能。

  • 栈分配:如果变量的生命周期仅限于函数内部,编译器会将其分配在栈上。栈分配速度快,且不需要垃圾回收。
  • 堆分配:如果变量的生命周期超出了函数范围(例如返回给调用者或存储到全局变量中),编译器会将其分配在堆上。堆分配需要垃圾回收器管理。

逃逸的常见场景

  • 函数内的变量被函数外使用:
    • 变量的地址被传递到函数外部(函数返回变量指针)
    • 函数内的变量赋值给全局变量
    • 函数内的变量传递给另一个函数
    • 变量被闭包捕获
  • 变量大小超出栈的范围:
    • 较大的变量可能分配到堆上,以避免栈溢出。
  • 变量的生命周期:
    • 生命周期较长的变量可能分配到堆上。

4. 堆栈分配的总结

特性 栈(Stack) 堆(Heap)
分配方式 编译器自动分配/释放(LIFO) 手动分配(如 new/make)或 GC 管理
生命周期 随函数调用结束自动销毁 需显式释放(或由 GC 回收)
访问速度 极快(CPU 缓存友好) 较慢(可能触发缺页中断)
内存碎片 无(严格顺序分配) 可能有(动态分配导致)
线程安全 每个线程独享栈 全局共享,需同步机制
典型存储内容 局部变量、函数参数、返回值 动态分配的对象(如 structslice)、逃逸对象

栈:每个 Goroutine 创建时会被分配一个 独立的栈(默认大小 2KB,随需动态增长,最大可达 1GB)。

堆:所有 Goroutine 共享堆:通过 newmake 或逃逸分析分配到堆的对象,可被多个 Goroutine 访问。

作者

ShiHaonan

发布于

2025-03-03

更新于

2025-04-21

许可协议

评论