Golang底层原理剖析之内存逃逸

简介: Golang底层原理剖析之内存逃逸

堆/栈

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

申请到栈内存好处:函数返回直接释放,不会引起垃圾回收,对性能没有影响。

申请到堆内存会引起垃圾回收,如果这个过程(特指垃圾回收不断被触发)过于高频就会导致 gc 压力过大,程序性能出问题。

逃逸分析

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

在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针。通俗地讲,逃逸分析就是确定一个变量要放堆上还是栈上,规则如下:

  1. 是否有在其他地方(非局部)被引用。只要有可能被引用了,那么它一定分配到堆上。否则分配到栈上。
  2. 即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上。
  3. 动态分配不定空间,编译器对于这种不定长度的申请方式,也会在堆上面申请,即使申请的长度很短。

所以逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为。

在函数中申请一个新的对象:

  • 如果分配在栈中,则函数执行结束可自动将内存回收;
  • 如果分配在堆中,则函数执行结束可交给GC(垃圾回收)处理;

为什么需要逃逸

如果变量都分配到堆上了会出现什么事情?例如:

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

怎么查看逃逸

通过编译器命令,就可以看到详细的逃逸分析过程

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

逃逸场景(什么情况才分配到堆中)

1. 返回局部变量地址(指针)

package main
type User struct {
  Name string
  Id int
}
func Test() *User{
  a:=&User{}
  return a
}
func main() {
  Test()
}
$ go build -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:9:5: &User{} escapes to heap

通过查看分析结果,可得知 &User 逃到了堆里,也就是分配到堆上了。

编译器认为函数外会用到这个局部变量,所以分配到堆上,如果分配到栈上,函数结束后,函数外引用的就是一个非法的地方了。

2. 编译阶段不能确定接口的动态类型(interface{})

package main
type Inter1 interface {
  A()
}
type Inter2 interface {
  B()
}
type User struct {
  Name string
  Id   int
}
func (u *User) A() {
}
func (u User) B() {
}
func Test1() interface{} {
  var a interface{}
  a = &User{}
  return a
}
func Test2() Inter1 {
  var b Inter1
  b = &User{}
  return b
}
func Test3() Inter2 {
  var b Inter2
  b = User{}
  return b
}
func main() {
  Test1()
  Test2()
  Test3()
}


$ go build -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:16:7: u does not escape
.\main.go:19:7: u does not escape
.\main.go:24:6: &User{} escapes to heap
.\main.go:30:5: &User{} escapes to heap
.\main.go:36:3: User{} escapes to heap
<autogenerated>:1: leaking param: .this
<autogenerated>:1: .this does not escape
<autogenerated>:1: leaking param: .this

编译阶段不能确定接口的动态类型,所以要分配在堆上

3. 栈空间不足导致逃逸(空间开辟过大)

package main
func Slice() {
  s := make([]int, 1000, 1000)
  for index, _ := range s {
    s[index] = index
  }
  s1 := make([]int, 1000, 10000)
  for index, _ := range s {
    s1[index] = index
  }
}
func main() {
  Slice()
}
$ go build -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:4:11: make([]int, 1000, 1000) does not escape
.\main.go:9:12: make([]int, 1000, 10000) escapes to heap

是否逃逸取决于栈空间是否足够大

4. 无法判断当前切片长度时

package main
func Slice() {
  len := 1
  s := make([]int, len, len)
  for index, _ := range s {
    s[index] = index
  }
}
func main() {
  Slice()
}
$ go build -gcflags '-m -l' main.go
# command-line-arguments
.\main.go:5:11: make([]int, len, len) escapes to heap

实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

func Test() {
        a := make([]int, 0, 20)     // 栈 空间小 不逃逸
        b := make([]int, 0, 20000) // 堆 空间过大 逃逸
        l := 20
        c := make([]int, 0, l) // 堆 动态分配不定空间 逃逸
}

5. 闭包捕获变量并修改导致变量逃逸

直接看专栏这篇博文Golang底层原理剖析之闭包

逃逸总结

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


目录
相关文章
|
7月前
|
机器学习/深度学习 存储 算法
NoProp:无需反向传播,基于去噪原理的非全局梯度传播神经网络训练,可大幅降低内存消耗
反向传播算法虽是深度学习基石,但面临内存消耗大和并行扩展受限的问题。近期,牛津大学等机构提出NoProp方法,通过扩散模型概念,将训练重塑为分层去噪任务,无需全局前向或反向传播。NoProp包含三种变体(DT、CT、FM),具备低内存占用与高效训练优势,在CIFAR-10等数据集上达到与传统方法相当的性能。其层间解耦特性支持分布式并行训练,为无梯度深度学习提供了新方向。
251 1
NoProp:无需反向传播,基于去噪原理的非全局梯度传播神经网络训练,可大幅降低内存消耗
|
20天前
|
安全 Go 开发者
“不要通过共享内存来通信”——深入理解Golang并发模型与CSP理论
Golang 采用 CSP 理念,主张“通过通信共享内存”,以消息传递替代共享内存,避免数据竞争。其核心为 Goroutine 与 Channel:轻量协程并发执行,通道安全传递数据,将并发复杂性转为通信编排,提升程序清晰度与可维护性。
121 0
|
6月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
219 0
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
197 4
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
225 3
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
262 3
|
8月前
|
Java 编译器 Go
go的内存逃逸分析
内存逃逸分析是Go编译器在编译期间根据变量的类型和作用域,确定变量分配在堆上还是栈上的过程。如果变量需要分配在堆上,则称作内存逃逸。Go语言有自动内存管理(GC),开发者无需手动释放内存,但编译器需准确分配内存以优化性能。常见的内存逃逸场景包括返回局部变量的指针、使用`interface{}`动态类型、栈空间不足和闭包等。内存逃逸会影响性能,因为操作堆比栈慢,且增加GC压力。合理使用内存逃逸分析工具(如`-gcflags=-m`)有助于编写高效代码。
158 2
|
9月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
425 0
|
存储 安全 测试技术
GoLang协程Goroutiney原理与GMP模型详解
本文详细介绍了Go语言中的Goroutine及其背后的GMP模型。Goroutine是Go语言中的一种轻量级线程,由Go运行时管理,支持高效的并发编程。文章讲解了Goroutine的创建、调度、上下文切换和栈管理等核心机制,并通过示例代码展示了如何使用Goroutine。GMP模型(Goroutine、Processor、Machine)是Go运行时调度Goroutine的基础,通过合理的调度策略,实现了高并发和高性能的程序执行。
669 29

热门文章

最新文章

推荐镜像

更多