程序中的数据和变量都会被分配到程序所在的虚拟内存中,内存空间包含两个重要区域:栈区(Stack)和堆区(Heap)。Go 语言的内存分配机制采用自动内存管理,由编译器和运行时协作完成。
- 编译器负责栈内存的分配和释放。
- 堆内存的分配则由运行时的内存分配器和垃圾收集器共同管理。
堆内存管理
程序在运行期间可以主动从堆区申请内存空间,这些内存由内存分配器分配并由垃圾收集器负责回收。Go 语言的内存分配器使用了多种策略来优化内存分配和回收的效率。
内存分配器
内存分配器的主要任务是将用户程序的内存申请请求转换为堆区的实际内存分配操作。当用户程序申请内存时,会通过内存分配器申请新内存,而内存分配器会负责从堆中初始化相应的内存区域。内存分配器的核心是一个内存池,它将堆区划分为多个小块,并根据需要动态分配和释放这些小块。
分配方法
内存分配器通常支持多种分配方法,如线性分配器和空闲链表分配器,以满足不同的内存分配需求。
线性分配器
线性分配器是一种简单高效的内存分配方法,它通过维护一个指针来跟踪当前分配的位置,每次分配时只需将指针向后移动一定的字节数即可。当用户程序向分配器申请内存时,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置。
- 优点是分配速度非常快,因为它只需要更新指针,而不需要进行复杂的内存管理操作。
- 缺点是无法释放已分配的内存,因此适用于那些生命周期较短的内存分配场景。
空闲链表分配器
空闲链表分配器是一种更复杂的内存分配方法,它维护一个空闲链表来跟踪未使用的内存块。当需要分配内存时,分配器会从空闲链表中找到合适的内存块并将其分配给用户程序。
- 优点是可以释放已分配的内存,从而更有效地利用堆区的内存资源。
- 缺点是分配速度相对较慢,因为需要在空闲链表中查找合适的内存块。
Go 内存分配方法
Go 语言的内存分配器采用了 TCMalloc(Thread-Caching Malloc)线程缓存分配,这是一种线程缓存的内存分配算法。它的核心理念是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。
内存分配管理的对象按照大小可以分为:
| 类别 | 大小 |
|---|---|
| 微对象 | (0, 16B) |
| 小对象 | [16B, 32KB] |
| 大对象 | (32KB, +∞) |
通过为每个线程维护一个本地缓存来减少锁的竞争,每个线程在分配内存时,首先尝试从本地缓存中获取内存,如果本地缓存中没有足够的内存,则向全局内存池申请新的内存块。这样可以减少线程之间的锁竞争,提高内存分配的效率。
点击展开
「图片来自一文搞懂 Go 内存分配器」
垃圾收集器
为了解决原始标记清除算法带来的长时间 STW,多数现代的追踪式垃圾收集器都会实现三色标记算法的变种以缩短 STW 的时间。
Go 语言的 GC 主要经历了以下三个重要阶段:
- Go1.3 的标记清除。
- Go1.5 并发三色标记法和插入写屏障。
- Go1.8 三色标记和混合写屏障机制。
其核心思想是优化 STW 时间,实现低停顿的并发垃圾回收,是其并发能力的核心支撑之一。
三色标记清除法
该算法的核心思想是通过对对象进行标记和分类来确定哪些对象是可达的,哪些对象是不可达的,从而进行垃圾回收。根据每个对象的颜色,分到不同的颜色集合中,对象的颜色是在标记阶段完成的。
将内存对象分为三类颜色:
- 白色(待回收态):表示对象还没有被标记,如果在垃圾回收结束时仍然是白色的,那么它将被回收。
- 灰色(中间态):表示对象已经被发现并标记,但其引用的子对象还没有被扫描,灰色对象需要进一步处理。
- 黑色(存活态):表示对象已经被扫描,并且所有引用的子对象也已经被标记,黑色对象为存活对象,不会被回收。
三色标记清除法的工作步骤:
- 初始化阶段
- 所有对象开始时都被标记为白色。
- 根对象被标记为灰色,并放入一个待处理队列中。
- 标记阶段
- 重复以下步骤,直到待处理队列为空:
- 从待处理队列中取出一个灰色对象,并将其标记为黑色。
- 遍历该对象的所有引用。如果被引用的对象是白色的,将其标记为灰色,并放入待处理队列中。
- 重复以下步骤,直到待处理队列为空:
- 清除阶段
- 遍历所有对象:
- 如果对象是白色的,说明它是不可达的,可以被回收。
- 如果对象是黑色的,说明它是可达的,保留不动。
- 遍历所有对象:
混合写屏障
在用户协程与 GC 协程并发执行的场景下,可能导致部分存活对象未被正确标记的情况。为了支持能够并发进行垃圾回收,Go 语言在垃圾回收过程中采用了混合屏障技术,在指针修改时触发,确保三色不变式不被破坏(没看明白,暂不做展开🤨)。
栈内存管理
栈区的内存一般由编译器自动分配和释放,其中存储着函数的入参以及局部变量,这些参数会随着函数的创建而创建,函数的返回而消亡,一般不会在程序中长期存在,这种线性的内存分配策略有着极高的效率。
逃逸分析
通过 -gcflags="-m" 编译器选项可以查看 Go 语言的逃逸分析结果。逃逸分析是编译器在编译阶段对变量的作用域进行分析,以确定变量是否需要分配到堆上。如果变量的作用域超出了函数的范围,或者需要在多个 Goroutine 中共享,则该变量会被分配到堆上,否则会分配到栈上。