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处理
  • 逃逸分析目的是决定内分配地址是栈还是堆
  • 逃逸分析在编译阶段完成


目录
相关文章
|
12天前
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
|
1月前
|
C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(二)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
编译器 C++ 开发者
【C++】深入解析C/C++内存管理:new与delete的使用及原理(三)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
1月前
|
存储 C语言 C++
【C++】深入解析C/C++内存管理:new与delete的使用及原理(一)
【C++】深入解析C/C++内存管理:new与delete的使用及原理
|
3月前
|
监控 算法 Java
Java内存管理:垃圾收集器的工作原理与调优实践
在Java的世界里,内存管理是一块神秘的领域。它像是一位默默无闻的守护者,确保程序顺畅运行而不被无用对象所困扰。本文将带你一探究竟,了解垃圾收集器如何在后台无声地工作,以及如何通过调优来提升系统性能。让我们一起走进Java内存管理的迷宫,寻找提高应用性能的秘诀。
|
12天前
|
存储 安全 测试技术
GoLang协程Goroutiney原理与GMP模型详解
本文详细介绍了Go语言中的Goroutine及其背后的GMP模型。Goroutine是Go语言中的一种轻量级线程,由Go运行时管理,支持高效的并发编程。文章讲解了Goroutine的创建、调度、上下文切换和栈管理等核心机制,并通过示例代码展示了如何使用Goroutine。GMP模型(Goroutine、Processor、Machine)是Go运行时调度Goroutine的基础,通过合理的调度策略,实现了高并发和高性能的程序执行。
75 29
|
10天前
|
负载均衡 算法 Go
GoLang协程Goroutiney原理与GMP模型详解
【11月更文挑战第4天】Goroutine 是 Go 语言中的轻量级线程,由 Go 运行时管理,创建和销毁开销小,适合高并发场景。其调度采用非抢占式和协作式多任务处理结合的方式。GMP 模型包括 G(Goroutine)、M(系统线程)和 P(逻辑处理器),通过工作窃取算法实现负载均衡,确保高效利用系统资源。
|
2月前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
3月前
|
NoSQL Java 测试技术
Golang内存分析工具gctrace和pprof实战
文章详细介绍了Golang的两个内存分析工具gctrace和pprof的使用方法,通过实例分析展示了如何通过gctrace跟踪GC的不同阶段耗时与内存量对比,以及如何使用pprof进行内存分析和调优。
83 0
Golang内存分析工具gctrace和pprof实战
|
3月前
|
算法 NoSQL 关系型数据库
熔断原理与实现Golang版
熔断原理与实现Golang版