当前位置: 首页 > news >正文

golang 内存逃逸 栈与堆区别

介绍

我们要聊的就是“内存逃逸”——这个看起来很恐怖的名字其实说白了就是,某个变量的生命周期可能被 Go 的编译器不小心“推”到了堆上,而不是栈上,导致了一些不必要的内存消耗。那么,内存逃逸到底是怎么回事,怎么避免它呢?

面试应该从以下角度回答

  • 什么是逃逸?
  • 导致内存逃逸的原因是什么
  • 常见的发生逃逸的情况与逃逸分析
  • 如何避免

栈和堆

  • 栈(Stack):栈内存是程序运行时的一个局部区域,用于存储局部变量和函数调用的上下文。栈上的内存分配和释放速度非常快,因为栈是一个先进后出的结构,函数调用结束后,栈上的内存可以立即回收。
  • 堆(Heap):堆内存是动态分配的内存区域,通常用于存储生命周期不确定的对象,例如使用 new 或 make 创建的对象。堆内存的管理相对复杂,需要垃圾回收(GC)来进行清理,回收速度不如栈快。

什么是内存逃逸?

内存逃逸,顾名思义,就是程序在执行过程中,某些本应在栈上分配的变量,被“逃逸”到了堆上。为什么会发生这种情况呢?
在 Go 语言中,如果一个变量的生命周期不再局限于当前函数内,或者说,它的作用域扩展到了函数外面,Go 的编译器就会认为这个变量需要存储在堆上,而不是栈上。这就是所谓的“内存逃逸”。

为什么内存逃逸有问题?

栈内存的分配和释放是非常高效的,因此,程序员通常希望将变量保存在栈上,因为栈上的内存会随着函数的执行自动释放。然而,如果 Go 编译器无法优化内存分配,导致变量被错误地分配到了堆上,就会产生以下问题:

  • 内存开销增加:堆内存的分配和释放比栈慢,因为它依赖垃圾回收机制。而且堆内存的管理成本较高,会带来性能上的浪费。

  • GC负担加重:内存逃逸意味着更多的内存需要由垃圾回收(GC)来管理。如果大量的变量逃逸到堆上,GC 会变得更加繁重,可能会导致频繁的垃圾回收,影响程序的响应时间。

  • 不必要的内存消耗:有些变量本来只是局部变量,完全可以在栈上分配,结果却因为逃逸而浪费了堆内存。

  • 指针引用和内存安全

  • 内存泄漏风险

变量逃逸的常见原因

指针传递:

  • 分配在栈上:当原生类型被取地址且地址被赋值给了一个指针变量,当这个指针变量只是在函数内部使用,则这个原生类型会被分配在栈上(即使是通过new方法分配的)

  • 变量逃逸到堆上的情况:如果这个指针变量被以某种形式作为了函数返回值(例如,指针变量是struct中的变量,struct是函数返回值),则这个原生类型被分配在堆上(原因很简单,如果分配在栈上,函数返回后栈中的数据失效,这个指针指向的地址就是无效的)

如果你在函数内部定义了一个局部变量,并返回了这个变量的指针,那么这个变量就会逃逸到堆上,因为在函数返回后,这个指针可能会被外部代码访问。

func createSlice() *[]int {
    s := make([]int, 10)
    return &s // 错误:返回局部变量的地址
}

正确的做法是直接返回切片本身,而不是其地址:

func createSlice() []int {
    s := make([]int, 10)
    return s // 正确:返回切片的副本
}

如果你将一个局部变量的指针传递给了其他函数,Go 编译器会推测这个变量的生命周期已经超出了当前作用域,从而将它分配到堆上。

package main

import "fmt"

func foo(ptr *int) {
    fmt.Println(*ptr)
}

func main() {
    x := 42
    foo(&x) // 这里传递了 x 的地址,x 会被分配到堆上
}

这个例子中,x 被传递给了 foo 函数,而 foo 是通过指针来访问 x 的。因为 Go
编译器无法确定 x 在 main 函数外部是否会继续使用,所以 x 被分配到了堆上

数组或切片的引用:

切片是一个动态数组,它分为

  • slice本身(即SliceHeader)
  • slice中的元素(即SliceHeader中Data)

与指针传递类似

  • 当返回指向slice的指针时,slice逃逸;
  • 当返回slice时,只有slice中的数据(Data)可能会逃逸(Data可能为地址)

具体

  • SliceHeader分配在栈上、Data分配在堆上
    当SliceHeader分配在栈上,Data既可以分配在栈上也可以分配在堆上

  • 当Data的空间不足、需要动态扩容时,Data会被分配在堆上
    当初始化slice时,Data所占空间达到64K时,SliceHeader和Data都会被分配在堆上(注意这里的64K边界是在自己的windows和linux机上测试到的,没有找go源码的出处,有可能不准确,理解为Data比较大时会直接分配在堆上比较好。另外除了slice,其他的数据类型如果初始化大小超过某个阈值时,应该也会直接分配在堆上)

  • 当SliceHeader分配在堆上,SliceHeader和Data都分配在堆上

代码示例:

package main

import "fmt"

func createSlice() []int {
    arr := [3]int{1, 2, 3} // arr 会逃逸到堆上
    return arr[:]
}

func main() {
    slice := createSlice()
    fmt.Println(slice)
}

在上面的代码中,arr 是一个局部变量,而它被切片的返回值所引用。因为返回的是切片,而不是数组,所以 arr 会被分配到堆上。

map

1)不作为函数返回值时,分配在栈上
2)作为函数返回值且返回的不是指针时,map的元素分配在堆上,map本身分配在栈上
3)作为函数返回值且返回的是指针时,map的元素分配在堆上,map本身也分配在堆上

闭包(Closure):

闭包是导致内存逃逸的一个典型场景。因为闭包会持有外部函数的引用,所以即使外部函数已经返回,闭包内部的变量依然可能在堆上存活。

代码示例:

package main

import "fmt"

func main() {
    f := func() int {
        x := 42 // 这个变量 x 会逃逸到堆上
        return x
    }
    fmt.Println(f())
}

这里,x 是一个局部变量,但因为 f 是一个闭包,Go 编译器不能确定 x 的生命周期是否结束,所以 x 被分配到了堆上。

将局部变量的地址存储在全局变量或外部包中:

如果你将局部变量的地址存储在全局变量或外部包中,那么这个局部变量也会逃逸到堆上。

var globalPtr *int
 
func setGlobalPtr() {
    local := 10
    globalPtr = &local // 错误:逃逸到堆上
}

正确的做法是使用指针指向一个新的堆分配的内存:

var globalPtr *int
 
func setGlobalPtr() {
    local := 10
    localPtr := new(int) // 在堆上分配内存
    *localPtr = local    // 赋值给堆上的内存
    globalPtr = localPtr // 正确:globalPtr 指向堆内存
}

传递给包含指针的接口:

如果你将一个局部变量的地址传递给一个接口(特别是包含指针方法的接口),那么这个变量可能会逃逸。

type MyInterface interface {
    DoSomething()
}
 
type MyStruct struct {
    value int
}
 
func (m *MyStruct) DoSomething() {}
 
func passToInterface(i MyInterface) {
    // i 可能持有对局部变量的引用,导致逃逸
}

如何避免内存逃逸?

为了提高程序的性能,减少内存消耗,避免内存逃逸是非常重要的。下面是一些常见的避免内存逃逸的技巧:

  • 尽量避免不必要的闭包:如果不是特别必要,尽量避免在函数内创建闭包,尤其是当闭包引用了大量外部变量时。闭包会把外部变量的生命周期拉长,导致这些变量被分配到堆上。

  • 避免指针传递:当传递变量的指针时,要尽量确保该变量的生命周期不会超出当前函数。如果能直接传值,而不是传指针,那就直接传值,避免不必要的堆分配。

  • 使用值传递而不是引用传递:对于小的、临时的结构体或数组类型,尽量采用值传递的方式,而不是传递其指针。这样,Go 编译器就可以安全地在栈上分配内存。

  • 避免返回切片或数组的引用:如果你只需要使用局部数据,尽量不要返回指向该数据的切片或数组。否则,它们的底层数组可能会逃逸到堆上。

如何检测内存逃逸?

Go 提供了 go run -gcflags=“-m” 命令来帮助我们检测内存逃逸问题。通过这个命令,我们可以查看编译器是否将某个变量分配到了堆上。

命令示例:

go run -gcflags="-m" main.go

当你运行这个命令时,Go 编译器会输出类似这样的信息:

/tmp/main.go:10:6: x escapes to heap

这表明变量 x 被分配到了堆上。

内存逃逸的优化

为了减少内存逃逸,可以通过优化代码来改善性能:

1)使用指针传递:将大的结构体或数组通过指针传递,避免复制数据。【需要修改原对象值,或占用内存比较大的结构体】
2)使用值传递:对于小的结构体或基本类型,可以使用值传递,避免指针的额外开销。【对于只读的占用内存较小的结构体】

相关文章:

  • 如何解决:http2: Transport received Server‘s graceful shutdown GOAWAY
  • qemu仿真调试esp32,以及安装版和vscode版配置区别
  • 字符串操作栈和队列
  • MES生产工单管理系统,Java+Vue,含源码与文档,实现生产工单全流程管理,提升制造执行效率与精准度
  • C++使用Qt Charts可视化大规模点集
  • matlab中排序函数sortrows的用法
  • 快速入手-前后端分离Python权限系统 基于Django5+DRF+Vue3.2+Element Plus+Jwt
  • SQL注入流量分析
  • 【算法】二分查找
  • 单片机实现触摸按钮执行自定义任务组件
  • IntelliJ IDEA下开发FPGA——FPGA开发体验提升__下
  • 量子计算模拟中的GPU加速:从量子门操作到Shor算法实现
  • 嵌入式硬件实战基础篇(三)-四层板PCB设计-步进电机驱动(TMC2208/TMC2209)
  • 双周报Vol.69: C FFI 支持 borrow、新增.mbt.md测试与调试、WASM 后端支持extern type..
  • Python----计算机视觉处理(Opencv:道路检测完整版:透视变换,提取车道线,车道线拟合,车道线显示,)
  • 解决:Fontconfig head is null, check your fonts or fonts configurat
  • Java设计模式之外观、享元、组合模式《三国争霸:模式风云录》
  • Spring MVC 框架 的核心概念、组件关系及流程的详细说明,并附表格总结
  • 【探商宝】 Llama 4--技术突破与争议并存的开源多模态
  • 【机器学习】ROC 曲线与 PR 曲线
  • 河南发布高温橙警:郑州、洛阳等地最高气温将达40℃以上
  • 上海这场有温度的“人才集市”,为更多人才搭建“暖心桥”
  • 网警打谣:传播涉刘国梁不实信息,2人被处罚
  • 多少Moreless:向世界展示现代中式家具的生活美学
  • 词条数量大幅扩充,《辞海》第八版启动编纂
  • 马上评|文玩字画竞拍轻松赚差价?严防这类新型传销