这是我参与「第五届青训营 」伴学笔记创作活动的第 11 天。
1. 协程栈
- Go 的协程栈位于堆内存中,堆内存位于操作系统的虚拟内存中。
- 栈帧中主要存储:函数的局部变量、参数、返回值。
- 函数的参数列表的实参是从右往左依次入栈的。
1.1. 逃逸分析
Go 编译器会尽可能将变量分配在栈上. 以下两种情况,Go 编译器会将变量分配在堆上:1. 如果一个变量被取地址,并且被逃逸分析为逃逸到堆;2. 该变量很大。
逃逸有三种方式:指针逃逸、空接口逃逸、大变量逃逸。
- 指针逃逸:函数返回了某个变量的指针或地址。
- 空接口逃逸:如果函数的形参为
interface{}(即变量类型不确定),函数的实参很可能会逃逸到堆上。 这是因为interface{}类型大概率会使用到反射。 - 大变量逃逸:大变量会消耗大量栈空间,导致栈空间不足。 64 位机器中,一般超过 64KB 的变量会逃逸到堆上。
1.2. 栈扩容
- Go 协程栈的初始空间为 2KB
- 每次在函数调用前会先执行
morestack来判断栈空间是否足够;不够需要进行栈扩容。
栈扩容机制:Go 1.13 之前使用分段栈;之后使用连续栈。
-
分段栈:在新位置开辟一片空间供栈使用
- 优点:没有空间浪费
- 缺点:若在扩容之处有多次的函数调用,那么就会反复地进行栈帧开辟和回收操作,栈指针会在不连续的位置来回跳转。
-
连续栈:在新的位置开辟一块更大(原来的 2 倍)的栈,将栈全部拷贝过来;当栈内存使用率低于 1/4 时,缩小栈为原来的一半。
- 优点:空间一直连续
- 缺点:伸缩时开销大
2. 堆
2.1. 操作系统虚拟内存
- 操作系统是不允许进程直接使用物理内存的。
- 操作系统为每个进程都提供了一块可供使用的虚拟的内存空间,由操作系统来将虚拟内存空间映射到物理内存或者外存(磁盘,Linux 称为 swap 区)中。
- 虚拟内存的大小是:32 位 4G,64 位 256T。
- Go 申请虚拟内存是一块一块申请的,每个单元被称为 heapArena,大小是 64M。
最多可以有 个这种 heapArena 虚拟内存单元。
所有的 heapArena 组成了 Go 的堆内存 mheap(刚好 )。
2.2. 堆内存分配策略
-
分级分配:把内存分为几种不同大小的块,将对象放入能容纳其的最小块中。
-
mspan:Go 中使用内存的最小单元为mspan,每个mspan是若干个相同大小的内存块。一共有 67 种mspan。 -
mcentral:为快速找到适合分配给对象的mspan,采用中心索引的方式。有 136 个mcentral个结构体,其中 68 个记录需要 GC 扫描的mspan,68 个记录不需要 GC 扫描的mspan -
对象分级
- 分级
- Tiny 微对象 0 - 16B, 无指针类型的属性
- Small 小对象 16B - 32B
- Large 大对象 32B 以上
- 微对象和小对象分配至普通
mspan;大对象量身定做mspan(class 0) - 将多个微对象合并为一个,存入一个 16B 的
mspan(class 2)中 - 小对象则寻找一个合适大小的
mspan来存放
- 分级
2.3. GC
垃圾收集算法:标记清除、标记整理、复制算法
由于 Go 使用的是分级分配内存的机制,并不会产生内存碎片的问题,所以使用的是标记清除算法。
根搜索算法 GC Roots Tracing
通过一系列名为 "GC Roots" 的对象作为起点,开始向下搜索(广度优先遍历 DFS)。当从 GC Roots 到一个对象不可达时,则该对象是不可用的。
Go 中的 GC Roots 包括:
- 被栈上指针所引用的对象
- 被全局变量指针所引用的对象
- 被寄存器中的指针所引用的对象