Go 语言的内存管理机制概览

Go 语言的内存管理机制概览

3月 9, 2021
内存管理, Golang

程序中的数据和变量在运行时会被分配到程序所在的虚拟内存中,内存空间包含两个重要的区域:堆(heap)和栈(stack),函数调用的参数、返回值以及局部变量大都会被分配到栈上,这部分内存由编译器进行管理。

用户程序(Mutator)通过内存分配器(Allocator)在堆上申请的内存,垃圾回收器(Collector)负责回收堆上的内存空间。可以说内存分配器和垃圾回收器共同管理着 Go 语言的堆内存。

Go 语言中,程序员不需要关心变量是存放在堆还是栈中,Go 语言编译器会通过逃逸分析等机制来做决定。

分配器 #

内存分配器主要分为线性分配器(Sequential Allocator)和空闲链表分配器(Free-List Allocator)两种,线性分配在内存管理中只需要维护一个指向内存特定位置的指针来区分开已使用和未使用的内存即可,而空闲链表分配器管理的可用内存在物理上经过多次回收可能是不连续的,需要由链表进行连接,需要分配内存时,分配器就会遍历链表找到足够大的内存块。

在上一篇 常见的垃圾回收算法 已经几种垃圾回收算法,其中标记压缩、复制回收和分代回收都是通过定期拷贝的方式整理内存碎片,将空闲内存合并,故都是采用线性分配器。而标记清除、引用计数、增量式垃圾回收是采用了空闲链表分配器。

分级分配 #

Go 语言内存分配器采用了现代 TCMalloc 内存分配算法的思想,会根据申请分配的内存大小选择不同的处理逻辑,运行时会根据对象的大小将对象分成微小(tiny)对象、小(small)对象和大(large)对象三种:

  • 微小对象(0,16B),只会分配给非指针类型的的对象。
  • 小对象(16B,32KB)
  • 大对象(32KB,$ +∞ $)

因为程序中的绝大多数对象大小都在 32KB 以下,通过多级分配分别处理大对象和小对象有利于提高内存分配器的性能。

为了管理这些不同大小的对象,Go 语言进一步将内存分为了 67 个级别的 span,其中 0 级代表大对象,当需要分配具体的对象时,并不是直接分配 span,而是分配不同级别的 span 中的元素。因此 span 的级别不是以 span 大小为依据,而是以 span 中元素的大小为依据。

type mspan struct {
	next *mspan     // next span in list, or nil if none
	prev *mspan     // previous span in list, or nil if none
	list *mSpanList // For debugging. TODO: Remove.

	startAddr uintptr // address of first byte of span aka s.base()
	npages    uintptr // number of pages in span

	manualFreeList gclinkptr // list of free objects in mSpanManual spans
	nelems uintptr // number of object in the span.
}

span 对应在源码中的数据结构就是 mspan,每个 mspan 会管理若干个大小为 8KB 的内存页。

多级缓存 #

对象的大小不同,处理方式也不一样,Go 语言分配器引入了线程缓存(mcache)、中心缓存(mcentral)和页堆(mheap)三个组件来实现分级管理内存。

mcache #

每个与线程绑定的逻辑处理器 P 都分配了一个 mcache,用于分配微小对象和小对象,mcache 持有所有级别的 mspan,但是每种级别只持有一个。如果协程需要内存可以直接从 mcache 中获取,由于在同一时间只有一个协程运行在逻辑处理器 P 上,所以中间不需要加锁。当 mcache 中的 mspan 不存在空闲对象时,就会从 mheap 持有的 mcentral 获取新的内存单元。与 mcache 不同,访问 mcentral 需要加互斥锁。

mcentral #

mcentral 对象需要收集所有给定规格大小的 mspan,每个 mcentral 都包含了两个 mspan 的链表,分别了包含空闲对象和不包含空闲对象的 mspan。这个做区分是为了更快地分配 mspan 给 mcache。每一个级别的 span 都会有一个 mcentral 用于管理 span 链表。而所有的 mcentral 组成一个数组,由 mheap 进行管理。

mheap #

mheap 不止管理 mcentral,大对象也会直接通过 mheap 进行分配。mheap 通过位图机制对线性地址空间实现了精确管理。

参考 #

https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/

https://mp.weixin.qq.com/s/TXJDVTexdMirOqdGJHCP-w

本文共 1408 字,上次修改于 Dec 6, 2024,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» 常见的垃圾回收算法

» Go 语言中的拷贝和传值

» 操作系统的内存管理机制

» Go 语言的 MPG 并发调度模型

» Go 语言的 Context 源码分析