当前位置: 首页 > 科技观察

Go内存分配与逃逸分析——理论

时间:2023-03-11 21:01:06 科技观察

前言大家好,我是杨哥。今天就和大家聊一聊Go语言的“内存分配”和“逃逸分析”。要理解GO的逃逸分析,首先要理解内存分配和栈:内存可以分配到堆上,也可以分配到栈上。GO语言是如何分配内存的?它的设计初衷和实现原理是什么?弄清以上问题,我们先来说说内存管理和堆、栈的知识点:内存管理内存管理主要包括两个动作:分配和释放。逃逸分析是为了分配内存,释放内存是GC的职责。栈在Go语言中,栈的内存是由编译器自动分配和释放的。栈区常存放函数参数、局部变量和调用函数帧。它们随着函数的创建而分配,随着函数destroy的退出而被删除。Go应用程序在运行时,每个goroutine都维护着自己的栈区,这个栈区只能自己使用,不能被其他goroutine使用。堆栈是调用堆栈的简称。一个栈通常包含许多栈帧(stackframe),描述了GC释放的函数之间的调用关系。在堆上分配时,必须找到一块足够大的内存来存储新的变量数据。在随后的释放中,垃圾收集器扫描堆空间以查找不再使用的对象。我们可以简单的理解为在用GO语言进行开发的过程中,要考虑的内存管理只是针对堆内存。在运行过程中,程序可以主动从堆中申请内存,由Go的内存分配器分配,由垃圾收集器回收。为了方便大家的理解,我们从以下几个角度来对比一下栈:堆和栈的对比。锁栈不需要加锁:每个goroutine都有自己的栈空间,也就是说不需要对栈进行内存操作。锁定。堆有时需要加锁:堆上的内存有时需要加锁,防止多线程冲突扩展知识点:堆上的内存为什么有时需要加锁?而不是总是需要锁定?因为Go的内存分配策略学习了TCMalloc的线程缓存思想,为每个处理器分配一个mcache。注意:来自mcache的内存分配也是无锁的。关注我,后面我会详细讲解这部分知识。性能堆栈内存管理良好的性能:堆栈上内存的分配和释放非常高效。简单的说,它只需要两条CPU指令:一条在栈上分配,一条在栈上释放。它只能借助与堆栈相关的寄存器来完成。堆内存管理性能差:程序堆上的内存回收,也有一个清除标记的阶段,比如Go使用的三色标记法。缓存策略栈缓存性能较好,堆缓存性能较差。原因是栈内存可以更好的利用CPU的缓存策略,因为栈空间比堆更连续。下面给大家介绍一下今天的重头戏:逃逸分析上面说了这么多堆和栈的知识点,目的是为了让大家更好的理解逃逸分析。上面说了,相对于把内存分配到堆上,分配到栈上的优势更加明显。Go语言也是如此:Go编译器会尽可能在栈上分配变量。但是,如果不能证明函数返回后变量没有被引用,则变量会分配在堆上,变量不会随着函数栈的回收而被回收。这样就避免了悬空指针的问题。另外,如果局部变量占用内存多,也会在堆上分配。Go如何判断内存分配在栈上还是堆上?答案是:逃逸分析。编译器使用逃逸分析技术来选择堆或栈。逃逸分析的基本思路是:检查变量的生命周期是否完全已知,如果通过则分配到栈上。否则,它被称为逃逸并且必须分配在堆上。逃逸分析原则虽然Go语言并没有明确说明逃逸分析原则,但有如下指导方针可供参考。不同于JAVAJVM的运行时逃逸分析,Go的逃逸分析是在编译时完成的:编译时无法确定的参数类型必须放在堆中;如果变量在函数外有引用,则必须放在堆中;如果一个变量占用大量内存时,首先放在堆上;如果变量在函数外没有被引用,则先入栈;逃逸分析示例我们使用这个命令来查看逃逸分析的结果:gobuild-gcflags'-m-m-l'1。参数是接口类型packagemainimport"fmt"funcmain(){a:=666fmt.Println(a)}运行结果原因分析是因为Println(a...interface{})参数是接口的{}类型,编译时无法确定具体的参数类型,所以内存分配到堆上。2.函数包外引用变量mainfunctest()*int{a:=10return&a}funcmain(){_=test()}运行结果变量a被函数外引用的原因分析。下面分析一下执行过程:函数执行时,对应的栈帧被销毁,但是函数外已经返回了引用。如果此时通过引用地址获取外部值,虽然地址还在,但是内存已经被释放回收了,就是非法内存。为了避免上述非法内存情况,这种情况下变量的内存分配必须在堆上分配。3.变量内存占用较多packagemainfunctest(){a:=make([]int,10000,10000)fori:=0;我<10000;i++{a[i]=i}}funcmain(){test()}运行结果分析我们定义了一个int类型的slice,容量为10000,转义,内存分配到堆上。注意:我们再简单修改一下代码,将slice的容量和长度改为1,再次查看逃逸分析的结果。我们发现并没有发生逃逸,内存默认分类在栈上。因此,当一个变量占用大量内存时,就会发生逃逸分析,将内存分配到堆中。4.当变量大小不确定时,我们简单修改上面的代码:packagemainfunctest(){l:=1a:=make([]int,l,l)fori:=0;我<我;i++{a[i]=i}}funcmain(){test()}运行结果分析通过控制台的输出我们可以清楚的看到:发生了逃逸,已经分配到堆中。原因是这样的:虽然我们在代码段中给变量l赋值了1,但是在编译时只能识别,在初始化一个int类型的slice时,传入的长度和容量就是变量l,而变量l的值在编译时无法确定,所以会发生逃逸,内存会分配到堆上。思维题做完了,我们列举了4个逃逸分析的经典案例,相信聪明的你已经了解了逃逸分析的作用和逃逸的场景。我们想一想,在了解了逃逸分析的原理之后,如何在开发过程中更好的编码,从而提高程序的运行效率,更好的利用内存呢?如何练习?了解逃逸分析,一定会帮助我们写出更好的程序。知道分配在栈上的变量的区别后,我们要尽量写出分配在栈上的代码。因为堆上的变量数量减少,可以减少内存分配的开销,减轻GC的压力,提高程序的运行速度。但我们也需要有一个过火的指导思想。我认为没有固定的发展模式。我们必须在不断变化的需求和业务变化中找到一个平衡点:举个日常开发中函数传参的例子:在某些场景下,我们不应该传递结构体指针,而应该直接传递结构体。为什么会这样?虽然直接传结构体需要进行值拷贝,但是这个操作是在栈上完成的,开销比变量逃逸后在堆上动态分配内存要小很多。当然这种做法也不是绝对的,要根据场景来分析:如果结构体比较大,传结构体指针比较合适,因为指针类型相对于值可以节省很多内存空间类型;如果结构体比较小,还是传结构体比较合适,因为在栈上分配内存可以有效降低GC压力小结通过本文的介绍,相信大家对栈有了更深入的了解;弄清楚逃逸分析的作用和原理后,可以指导我们写出更优雅的代码。在我们日常的开发中,应该考虑如何根据实际场景尽可能的给栈分配内存,以减轻GC的压力,提高性能。如何找到应用开发效率、程序运行效率、机器压力和负载的平衡点,是程序员进阶之旅的必修课。本文转载自微信公众号《程序员的升级打怪之旅》,作者“王中阳围棋”,可通过以下二维码关注。转载本文请联系《程序员升级打怪之旅》公众号。