🌺每天分享一些包括但不限于计算机基础、算法等相关的知识点🌺
💗点关注不迷路,总有一些📖知识点📖是你想要的💗
⛽️今天的内容是 go的内存管理和内存逃逸 ⛽️💻💻💻
Go 借鉴了 Google 的 TCMalloc(高性能的、用于 c++ 的内存分配器)。其核心思想是内存池 + 多级对象管理 ,能加快分配速度,降低资源竞争。
几个关键数据结构在 Go 里用于内存管理的对象结构主要是:mcache、mcentral、mheap、arenas&&mspan。
其中,mspan 是一个基础结构,分配内存时,基本以它为单位。
mcache、mcentral、mheap 起到了内存池的作用,会被预分配内存,当有对应大小的对象需要分配时会先到它们这一层请求。如果这一层内存池不够用时,会按照【mcache -> mcentral-> mheap -> 操作系统】顺序一层一层的往上申请内存。
mcache
与P(可看做cpu)绑定的线程级别的本地缓存。mcentral
全局空间的缓存,收集了各种大小(67种)的span列表mheap
是一个全局变量,会在系统初始化的时候初始化,分配内存的堆分配器,以8kb进行页管理mspan
由mheap管理的页面,记录了所分配的块大小和起始地址等arenas
是动态分配的堆区,它将分配到的内存以 8k 为一页进行管理。
fixalloc:固定尺寸的堆外对象空闲列表分配器,用来管理分配器的存储
对象结构关系如下:
mspan&&arenas
先来看看 mspan 这个基础结构体。首先,当 Go 在程序初始化的时候,会将申请到的虚拟内存划分为以下三个部分:
arenas 也就是动态分配的堆区,它将分配到的内存以 8k 为一页进行管理。
然而 “页” 这个单位还是太细了,因此再抽象出 mspan 这一层来管理,mspan 表示一组连续的页面。
mspan 记录了这组连续页面的起止地址、页数量、以及类型规格。
关于 mspan 的类型规格有 67 种,每一种都被定义了一个固定大小,当有对象需要分配内存时,就会挑选合适规格的 mspan 分配给对象。
bitmap 主要用来标记 arena 区域中哪些地址保存了对象, GC 扫描信息以及对象指针信息。
总体上来讲,spans 和 bitmap 区域可以看做是 arenas 区域的元数据信息,辅助内存管理。
mheap && mcentral
mheap 在 Go 里是一个全局对象,用来管理大于 32K 对象的内存分配。
mcentral 维护了各个规格的 mspan。当它的下级 mcache 内存不足时,则会到 mcentral 这里来申请 mspan。由于 mcentral 有各个规格类型的 mspan,因此当有不同规格的分配请求时,并不会产生并发竞争的问题。只有当同类型规格的 mspan 并发请求分配时,才会有加锁操作。
mcache
mcache 是提供给 P 的本地内存池。(关于 GPM 模型可以看参考:Go语言的GPM调度器是什么?_李歘歘的博客-CSDN博客),由于每次只会有一个 Goroutine 在 P 上执行。这么做的好处是分配内存时不需要加锁,所以分配内存是不需要竞争的。
mcache 上还有微型分配器,当要分配更小元素:即 <= 16B 时,会在一个 8byte 的 mspan 上分配多个的对象,这样就能更好的利用内存空间。
freelarge
这种设计之所以快,主要有以下几个优势:
- 内存分配大多时候都是在用户态完成的,不需要频繁进入内核态。
- 每个 P 都有独立的 span cache,多个 CPU 不会并发读写同一块内存,进而减少 CPU L1 cache 的 cacheline 出现 dirty 情况,增大 cpu cache 命中率。
- 内存碎片的问题,Go 是自己在用户态管理的,在 OS 层面看是没有碎片的,使得操作系统层面对碎片的管理压力也会降低。
- mcache 的存在使得内存分配不需要加锁。
当然这不是没有代价的,Go 需要预申请大块内存,这必然会出现一定的浪费,不过好在现在内存比较廉价,不用太在意。
总体上来看,Go 内存管理也是一个金字塔结构:
总结一下发生逃逸的结论:
什么是内存逃逸?
- 首先明确一点,Golang中所有的数据都是按值传递,这点和C语言是一样的。所谓的map、slice和chan等是引用类型,但是依然是值传递,其本质原因是,这些结构的内部都有指针,复制的时候,内部都是复制的指针,因此表现的是传值。
- Go 有两个地方可以分配内存:一个全局堆空间用来动态分配内存,另一个是每个 goroutine 都有的自身栈空间。
- 在函数调用中,对于指针的情况,只要指向的地址的所有者只有一个,那么必然是栈回收;而一旦存在地址存在不确定变化时,则转换成堆的数据。比如slice情况,因为slice会扩容或者缩容,因此造成不确定情况。
- Go 更倾向于在栈空间上分配内存: 一个 Go 程序大部分的内存分配都是在栈空间上的。它的代价很低,因为只需要两个 CPU 指令:一个是把数据 push 到栈空间上以完成分配,另一个是从栈空间上 pop 出去。
答案:当发生内存逃逸时,对象从栈中逃逸到了堆中,就需要用户自己回收了【其实是内存回收自动】
堆和栈是内存存储的一种结构
- 堆(heap) 用于动态分配内存,由程序员申请分配和释放,在go里面内存是自动回收的,有自己的一套垃圾回收机制(gc)进行回收,gc的好处在于不必过多关注于内存的管理引发的一系列内存泄漏的问题,但不好也在于gc也伴随着性能的消耗,GC内容参考:go的内存管理和内存逃逸_李歘歘的博客-CSDN博客。
- 栈(stack)是一种线性的存储结构,栈区的内存一般由编译器分配和释放,一般存储着函数的入参以及局部变量,这些参数和局部变量会随着整个函数生命周期结束而被销毁。
栈在分配和回收内存的开销很低,只需要2个CPU指令:PUSH和POP,而堆方面一个很大的开销在于垃圾回收。
go怎么知道一个对象是应该放在堆内存,还是栈内存之上呢?
答案:编译器通过逃逸分析技术去选择对象分配在堆还是栈,逃逸分析的基本思想如下:golang程序变量会携带有一组校验数据,检查变量的生命周期是否是完全可知的,如果通过检查,则可以在栈上分配。否则,就是所谓的逃逸,必须在堆上进行分配。
Go语言虽然没有明确说明逃逸分析规则,但是有以下几点准则,可以参考的:
- 逃逸分析是在编译时完成的,这是不同于jvm的运行时逃逸分析;
- 如果变量在函数外部没有引用,则优先放到栈中;
- 如果变量在函数外部存在引用,则必定放在堆中;
- 栈上分配内存比在堆中分配内存有更高的效率
- 栈上分配的内存不需要GC处理
- 堆上分配的内存使用完毕会交给GC处理
- 逃逸分析目的是决定内存分配地址是栈还是堆
所以传递值不一定比传递指针慢,因为值是栈中处理,速度快,而指针是逃逸到堆中,处理慢,gc多,所以自己掂量啊。
引起变量逃逸到堆上的典型情况:
goroutinechannel[]*string
参考: