2021年3月23日-golang内存管理基础组件了解
2108
2021.03.24
2021.03.25
发布于 未知归属地

golang内存管理

文章还存在几个问题没有解答,先放在这里

问题

heapArena

https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#%e7%a8%80%e7%96%8f%e5%86%85%e5%ad%98

稀疏内存中 的每个 heapArena

type heapArena struct {
	bitmap       [heapArenaBitmapBytes]byte
	spans        [pagesPerArena]*mspan
	pageInUse    [pagesPerArena / 8]uint8
	pageMarks    [pagesPerArena / 8]uint8
	pageSpecials [pagesPerArena / 8]uint8
	checkmarks   *checkmarksMap
	zeroedBase   uintptr
}

不同平台和架构的二维数组大小可能完全不同,如果我们的 Go 语言服务在 Linux 的 x86-64 架构上运行,二维数组的一维大小会是 1,而二维大小是 4,194,304,因为每一个指针占用 8 字节的内存空间,所以元信息的总大小为 32MB。由于每个 runtime.heapArena 都会管理 64MB 的内存,整个堆区最多可以管理 256TB 的内存,这比之前的 512GB 多好几个数量级。

问题1 信息元不是heapArena吗。为何说信息元总大小为32mb,而heapArena管理64mb呢

问题2 整个堆区最多可以管理 256TB,这个数字哪里来的呀?

跨度类

https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#%e8%b7%a8%e5%ba%a6%e7%b1%bb

在跨度类5中

5 48 8192 170 32 31.52%

上表展示了对象大小从 8B 到 32KB,总共 67 种跨度类的大小、存储的对象数以及浪费的内存空间,以表中的第四个跨度类为例,跨度类为 5 的 runtime.mspan 中对象的大小上限为 48 字节、管理 1 个页、最多可以存储 170 个对象。因为内存需要按照页进行管理,所以在尾部会浪费 32 字节的内存,当页中存储的对象都是 33 字节时,最多会浪费 31.52% 的资源:

((48−33)∗170+32)/8192=0.31518

问题 这个33字节是哪来的?

堆和栈

堆和栈的区别

为何堆的使用比栈的使用开销大?

栈上分配空间代价很低,自动由编译器进行管理,只需要两个 CPU 指令:一个是把数据 push 到栈空间上以完成分配,另一个是从栈空间上释放。

Go 更倾向于在栈空间上分配内存 —— 一个 Go 程序大部分的内存分配都是在栈空间上的。

不同编程语言使用不同的方法管理堆区的内存,C++ 等编程语言会由工程师主动申请和释放内存,Go 以及 Java 等编程语言会由工程师和编译器共同管理,堆中的对象由内存分配器分配并由垃圾收集器回收。
为何栈分配比较快?
具体来说,现代计算机,都直接在代码底层支持栈的数据结构。这体现在,有专门的寄存器指向栈所在的地址,有专门的机器指令完成数据入栈出栈的操作。这种机制的特点是效率高,支持的数据有限,一般是整数,指针,浮点数等系统支持的数据类型,并不直接支持其他的数据结构。

简单点说也就是因为我们可以直接调用CPU指令堆栈空间进行分配

堆和栈的选择

逃逸分析 是一种确定指针动态范围的方法,简单来说就是分析在程序的哪些地方可以访问到该指针

通俗地讲,逃逸分析就是确定一个变量要放堆上还是栈上,规则如下:

  • 是否有在其他地方(非局部)被引用。只要有可能被引用了,那么它一定分配到堆上。否则分配到栈上
  • 即使没有被外部引用,但对象过大,无法存放在栈区上。依然有可能分配到堆上

逃逸分析的时间 在编译时就进行逃逸分析,go build -gcflags '-m -l'

逃逸分析的原因 栈分配的开销小,而堆的开销大,逃逸分析就是为了正确选择堆还是栈从而进行节约资源。

哪些会被分配到到栈

  • 如果局部变量在函数外部没有引用,则优先放到栈中;
  • 如果局部变量在函数外部存在引用,则必定放在堆中;
  • 如果局部变量在函数外部没有引用,但是作为interface{}传参,则逃逸到堆中-比如fmt.Print(),因为fmt.Println的函数参数为interface类型,编译期不能确定其参数的具体类型,所以将其分配于堆上。
  • 如果局部变量在函数外部没有引用,但是变量比较大,-通过命令ulimit -s可以查看栈的限制;
  • **为何要对变量大的进行限制?**以x86_64架构为例,它的系统栈大小最大可为8Mb。我们常说的goroutine初始大小为2kb,其实说的是用户栈,它的最小和最大可以在runtime/stack.go中找到,分别是2KB和1GB。在Go中,执行用户代码的goroutine是一种用户态线程,其调用栈内存被称为用户栈,它其实也是从堆区分配的,但是我们仍然可以将其看作和系统栈一样的内存空间,它的分配和释放是通过编译器完成的。与其相对应的是系统栈,它的分配和释放是操作系统完成的。在GMP模型中,一个M对应一个系统栈(也称为M的g0栈),M上的多个goroutine会共享该系统栈。Go采用了稀疏的内存布局,在Linux的x86-64架构上运行时,整个堆区最大可以管理到256TB的内存。所以,为了不造成栈溢出和频繁的扩缩容,大的对象分配在堆上更加合理。那么,多大的对象会被分配到堆上呢。
  • 如果局部变量的大小不确定比如make([]int, n),也会进行逃逸到堆中-为了保证内存的绝对安全,Go的编译器可能会将一些变量不合时宜地分配到堆上。

确定变量的大小可以减小资源开销

无用数据不进行返回可减小资源开销

传参变量尽量确定类型可减小资源开销

传递结构体的复制体而不是指针可以减小资源开销-拷贝赋值是在栈上进行的

内存分配-分配器

内存管理一般包含三个模块,分别是用户程序(Mutator)、分配器(Allocator)和收集器(Collector) 当用户程序申请内存时,它会通过内存分配器申请新内存,而分配器会负责从堆中初始化相应的内存区域

分配方法
编程语言的内存分配器一般包含两种分配方法,一种是线性分配器(Sequential Allocator,Bump Allocator),另一种是空闲链表分配器(Free-List Allocator),这两种分配方法有着不同的实现机制和特性,本节会依次介绍它们的分配过程。

线性分配器

线性分配(Bump Allocator)是一种高效的内存分配方法,但是有较大的局限性。当我们使用线性分配器时,只需要在内存中维护一个指向内存特定位置的指针,如果用户程序向分配器申请内存,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置

虽然线性分配器实现为它带来了较快的执行速度以及较低的实现复杂度,但是线性分配器无法在内存被释放时重用内存。如下图所示,如果已经分配的内存被回收,线性分配器无法重新利用已经被释放的内存

无法重新利用需要引入新的垃圾回收算法:标记压缩(Mark-Compact)、复制回收(Copying GC)、分代回收(Generational GC)等算法

空闲链表分配器

空闲链表分配器(Free-List Allocator)可以重用已经被释放的内存,它在内部会维护一个类似链表的数据结构。当用户程序申请内存时,空闲链表分配器会依次遍历空闲的内存块,找到足够大的内存,然后申请新的资源并修改链表:

所以使用这种方式的分配器可以重新利用回收的资源,但是因为分配内存时需要遍历链表,所以它的时间复杂度是 O(n)。空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择,最常见的是以下四种:

  • 首次适应(First-Fit)— 从链表头开始遍历,选择第一个大小大于申请内存的内存块;
  • 循环首次适应(Next-Fit)— 从上次遍历的结束位置开始遍历,选择第一个大小大于申请内存的内存块;
  • 最优适应(Best-Fit)— 从链表头遍历整个链表,选择最合适的内存块;
  • 隔离适应(Segregated-Fit)— 将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再从链表中选择合适的内存块;

golang 分配器的分级分配

线程缓存分配(Thread-Caching Malloc,TCMalloc)是用于分配内存的机制,它比 glibc 中的 malloc 还要快很多2。Go 语言的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心理念是使用多级缓存将对象根据大小分类,并按照类别实施不同的分配策略。

Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种:

类别	大小
微对象	(0, 16B)
小对象	[16B, 32KB]
大对象	(32KB, +∞)

因为程序中的绝大多数对象的大小都在 32KB 以下,而申请的内存大小影响 Go 语言运行时分配内存的过程和开销,所以分别处理大对象和小对象有利于提高内存分配器的性能。

线程缓存属于每一个独立的线程,它能够满足线程上绝大多数的内存分配需求,因为不涉及多线程,所以也不需要使用互斥锁来保护内存,这能够减少锁竞争带来的性能损耗。当线程缓存不能满足需求时,运行时会使用中心缓存作为补充解决小对象的内存分配,在遇到 32KB 以上的对象时,内存分配器会选择页堆直接分配大内存。

这种多层级的内存分配设计与计算机操作系统中的多级缓存有些类似,因为多数的对象都是小对象,我们可以通过线程缓存和中心缓存提供足够的内存空间,发现资源不足时从上一级组件中获取更多的内存资源。

虚拟内存布局

线性内存

spans 区域存储了指向内存管理单元 runtime.mspan 的指针,每个内存单元会管理几页的内存空间,每页大小为 8KB;
bitmap 用于标识 arena 区域中的那些地址保存了对象,位图中的每个字节都会表示堆区中的 32 字节是否包含空闲;
arena 区域是真正的堆区,运行时会将 8KB 看做一页,这些内存页中存储了所有在堆上初始化的对象;

如下所示的三个区域 spans、bitmap 和 arena 分别预留了 512MB、16GB 以及 512GB 的内存空间,这些内存并不是真正存在的物理内存,而是虚拟内存:[[spans][bitmap][arena]]

  • spans 区域存储了指向内存管理单元 runtime.mspan 的指针,每个内存单元会管理几页的内存空间,每页大小为 8KB;

  • bitmap 用于标识 arena 区域中的那些地址保存了对象,位图中的每个字节都会表示堆区中的 32 字节是否包含空闲;

  • arena 区域是真正的堆区,运行时会将 8KB 看做一页,这些内存页中存储了所有在堆上初始化的对象;

Go 语言在垃圾回收时会根据指针的地址判断对象是否在堆中,并通过上一段中介绍的过程找到管理该对象的 runtime.mspan。这些都建立在堆区的内存是连续的这一假设上。这种设计虽然简单并且方便,但是在 C 和 Go 混合使用时会导致程序崩溃:

  1. 分配的内存地址会发生冲突,导致堆的初始化和扩容失败
  2. 没有被预留的大块内存可能会被分配给 C 语言的二进制,导致扩容后的堆不连续

线性的堆内存需要预留大块的内存空间,但是申请大块的内存空间而不使用是不切实际的,不预留内存空间却会在特殊场景下造成程序崩溃。虽然连续内存的实现比较简单,但是这些问题也没有办法忽略。

稀疏内存

稀疏内存是 Go 语言在 1.11 中提出的方案,使用稀疏的内存布局不仅能移除堆大小的上限5,还能解决 C 和 Go 混合使用时的地址空间冲突问题6。不过因为基于稀疏内存的内存管理失去了内存的连续性这一假设,这也使内存管理变得更加复杂:

[heapArena ,heapArena ,heapArena ,heapArena ,heapArena ,heapArena ...]

type heapArena struct {
	bitmap       [heapArenaBitmapBytes]byte
	spans        [pagesPerArena]*mspan
	pageInUse    [pagesPerArena / 8]uint8
	pageMarks    [pagesPerArena / 8]uint8
	pageSpecials [pagesPerArena / 8]uint8
	checkmarks   *checkmarksMap
	zeroedBase   uintptr
}

运行时使用runtime.heapArena 二维数组管理所有的内存,每个单元runtime.heapArena都会管理 64MB 的内存空间:

该结构体中的 bitmap 和 spans 与线性内存中的 bitmap 和 spans 区域一一对应,zeroedBase 字段指向了该结构体管理的内存的基地址。上述设计将原有的连续大内存切分成稀疏的小内存,而用于管理这些内存的元信息也被切成了小块。

不同平台和架构的二维数组大小可能完全不同,如果我们的 Go 语言服务在 Linux 的 x86-64 架构上运行,二维数组的一维大小会是 1,而二维大小是 (4,194,304)4*1024*1024个,因为每一个指针占用 8 字节的内存空间,所以元信息的总大小为 32MB。由于每个 runtime.heapArena 都会管理 64MB 的内存,整个堆区最多可以管理 256TB 的内存,这比之前的 512GB 多好几个数量级。

Go 语言团队在 1.11 版本中通过以下几个提交将线性内存变成稀疏内存,移除了 512GB 的内存上限以及堆区内存连续性的假设

由于内存的管理变得更加复杂,上述改动对垃圾回收稍有影响,大约会增加 1% 的垃圾回收开销,不过这也是我们为了解决已有问题必须付出的成本

golang os的地址空间内存管理抽象层

该抽象层将运行时管理的地址空间分成以下四种状态

状态	解释
None	内存没有被保留或者映射,是地址空间的默认状态
Reserved	运行时持有该地址空间,但是访问该内存会导致错误
Prepared	内存被保留,一般没有对应的物理内存访问该片内存的行为是未定义的可以快速转换到 Ready 状态
Ready	可以被安全访问

每个不同的操作系统都会包含一组用于管理内存的特定方法,这些方法可以让内存地址空间在不同的状态之间转换,我们可以通过下图了解不同状态之间的转换过程:

None = runtime.sysFree(Reserved)
None = runtime.sysFree(Prepared)
None = runtime.sysFree(Ready)

Ready = runtime.sysAlloc(None)
Ready = runtime.sysUsed(Prepared)

Prepared = runtime.sysUnused(Ready)
Prepared = runtime.sysMap(Reserved)

Reserved = runtime.sysReserve(None)
Reserved = runtime.sysFault(Prepared)
Reserved = runtime.sysFault(Ready)

运行时中包含多个操作系统实现的状态转换方法,所有的实现都包含在以 mem_ 开头的文件中,本节将介绍 Linux 操作系统对上图中方法的实现:

  • runtime.sysAlloc 会从操作系统中获取一大块可用的内存空间,可能为几百 KB 或者几 MB;
  • runtime.sysFree 会在程序发生内存不足(Out-of Memory,OOM)时调用并无条件地返回内存;
  • runtime.sysReserve 会保留操作系统中的一片内存区域,访问这片内存会触发异常;
  • runtime.sysMap 保证内存区域可以快速转换至就绪状态;
  • runtime.sysUsed 通知操作系统应用程序需要使用该内存区域,保证内存区域可以安全访问;
  • runtime.sysUnused 通知操作系统虚拟内存对应的物理内存已经不再需要,可以重用物理内存;
  • runtime.sysFault 将内存区域转换成保留状态,主要用于运行时的调试;

运行时使用 Linux 提供的 mmap、munmap 和 madvise 等系统调用实现了操作系统的内存管理抽象层,抹平了不同操作系统的差异,为运行时提供了更加方便的接口,除了 Linux 之外,运行时还实现了 BSD、Darwin、Plan9 以及 Windows 等平台上抽象层。

go内存管理组件

https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E5%8D%95%E5%85%83

Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件,本节将介绍这几种最重要组件对应的数据结构 runtime.mspan、runtime.mcache、runtime.mcentral 和 runtime.mheap,我们会详细介绍它们在内存分配器中的作用以及实现。

go内存管理组件的布局

[[mcahe],[mcahe],[mcahe],[mcahe],[mcahe]...]
当内存管理单元中不存在空闲对象时,从 `runtime.mheap` 持有的 134 个中心缓存 `runtime.mcentral` [[mcentral],[mcentral],[mcentral],[mcentral],[mcentral]...]*134 中获取新的内存单元
[一个mheap]
[稀疏内存]/[heaparena,heaparena,heaparena,heaparena,heaparena,heaparena...]

所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局,每一个处理器都会分配一个线程缓存 runtime.mcache 用于处理微对象和小对象的分配,它们会持有内存管理单元 runtime.mspan

每个类型的内存管理单元都会管理特定大小的对象,当内存管理单元中不存在空闲对象时,它们会从 runtime.mheap 持有的 134 个中心缓存 runtime.mcentral 中获取新的内存单元,中心缓存属于全局的堆结构体 runtime.mheap,它会从操作系统中申请内存。

在 amd64 的 Linux 操作系统上,runtime.mheap 会持有 4,194,304 runtime.heapArena,每个 runtime.heapArena 都会管理 64MB 的内存,单个 Go 语言程序的内存上限也就是 256TB。

内存管理单元 #
runtime.mspan 是 Go 语言内存管理的基本单元,该结构体中包含 next 和 prev 两个字段,它们分别指向了前一个和后一个 runtime.mspan

type mspan struct {
	next *mspan
	prev *mspan
	...
}

内存管理单元与对象在垃圾回收是

如果我们能在内存中找到空闲的内存单元会直接返回,当内存中不包含空闲的内存时,上一级的组件 runtime.mcache 会为调用 runtime.mcache.refill 更新内存管理单元以满足为更多对象分配内存的需求。

状态

运行时会使用 runtime.mSpanStateBox 存储内存管理单元的状态 runtime.mSpanState:

type mspan struct {
	...
	state       mSpanStateBox
	...
}

该状态可能处于 mSpanDead、mSpanInUse、mSpanManual 和 mSpanFree 四种情况。当 runtime.mspan 在空闲堆中,它会处于 mSpanFree 状态;当 runtime.mspan 已经被分配时,它会处于 mSpanInUse、mSpanManual 状态,运行时会遵循下面的规则转换该状态:

  • 在垃圾回收的任意阶段,可能从 mSpanFree 转换到 mSpanInUse 和 mSpanManual;
  • 在垃圾回收的清除阶段,可能从 mSpanInUse 和 mSpanManual 转换到 mSpanFree;
  • 在垃圾回收的标记阶段,不能从 mSpanInUse 和 mSpanManual 转换到 mSpanFree;
    设置 runtime.mspan 状态 必须是原子性(如果把一个事务可看作是一个程序,它要么完整的被执行,要么完全不执行。这种特性就叫原子性。)的以避免垃圾回收造成的线程竞争问题。

线程缓存与内存管理单元

线程缓存在刚刚被初始化时是不包含 runtime.mspan 的,只有当用户程序申请内存时才会从上一级组件获取新的 runtime.mspan 满足内存分配的需求。
运行时在初始化处理器时会调用 runtime.allocmcache 初始化线程缓存,该函数会在系统栈中使用 runtime.mheap 中的线程缓存分配器初始化新的 runtime.mcache 结构体:

func allocmcache() *mcache {
	var c *mcache
	systemstack(func() {
		lock(&mheap_.lock)
		c = (*mcache)(mheap_.cachealloc.alloc())
		c.flushGen = mheap_.sweepgen
		unlock(&mheap_.lock)
	})
	for i := range c.alloc {
		c.alloc[i] = &emptymspan
	}
	c.nextSample = nextSample()
	return c
}

就像我们在上面提到的,初始化后的 runtime.mcache 中的所有 runtime.mspan 都是空的占位符 emptymspan

中心缓存 runtime.mcentral

runtime.mcentral 是内存分配器的中心缓存,与线程缓存不同,访问中心缓存中的内存管理单元需要使用互斥锁:

type mcentral struct {
	spanclass spanClass
	partial  [2]spanSet
	full     [2]spanSet
}

Go 每个中心缓存都会管理某个跨度类的内存管理单元,它会同时持有两个 runtime.spanSet,分别存储包含空闲对象和不包含空闲对象的内存管理单元。

内存管理单元

线程缓存会通过中心缓存的 runtime.mcentral.cacheSpan 方法获取新的内存管理单元,该方法的实现比较复杂,我们可以将其分成以下几个部分:

  • 调用 runtime.mcentral.partialSwept 从清理过的、包含空闲空间的 runtime.spanSet 结构中查找可以使用的内存管理单元;
  • 调用 runtime.mcentral.partialUnswept 从未被清理过的、有空闲对象的 runtime.spanSet 结构中查找可以使用的内存管理单元;
  • 调用 runtime.mcentral.fullUnswept 获取未被清理的、不包含空闲空间的 runtime.spanSet 中获取内存管理单元并通过 runtime.mspan.sweep 清理它的内存空间;
  • 调用 runtime.mcentral.grow 从堆中申请新的内存管理单元;
    更新内存管理单元的 allocCache 等字段帮助快速分配内存;

首先我们会在中心缓存的空闲集合中查找可用的 runtime.mspan,运行时总是会先从获取清理过的内存管理单元,后检查未清理的内存管理单元:

func (c *mcentral) cacheSpan() *mspan {
	sg := mheap_.sweepgen
	spanBudget := 100

	var s *mspan
	if s = c.partialSwept(sg).pop(); s != nil {
		goto havespan
	}

	for ; spanBudget >= 0; spanBudget-- {
		s = c.partialUnswept(sg).pop()
		if s == nil {
			break
		}
		if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
、			s.sweep(true)
			goto havespan
		}
	}
	...
}

当找到需要回收的内存单元时,运行时会触发 runtime.mspan.sweep 进行清理,如果在包含空闲空间的集合中没有找到管理单元,那么运行时尝试会从未清理的集合中获取:

func (c *mcentral) cacheSpan() *mspan {
	...
	for ; spanBudget >= 0; spanBudget-- {
		s = c.fullUnswept(sg).pop()
		if s == nil {
			break
		}
		if atomic.Load(&s.sweepgen) == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
、			s.sweep(true)
、			freeIndex := s.nextFreeIndex()
			if freeIndex != s.nelems {
				s.freeindex = freeIndex
				goto havespan
			}
、			c.fullSwept(sg).push(s)
		}
、	}
	...
}

如果 runtime.mcentral 通过上述两个阶段都没有找到可用的单元,它会调用 runtime.mcentral.grow 触发扩容从堆中申请新的内存:

func (c *mcentral) cacheSpan() *mspan {
	...
	s = c.grow()
	if s == nil {
		return nil
	}

havespan:
	freeByteBase := s.freeindex &^ (64 - 1)
	whichByte := freeByteBase / 8
	s.refillAllocCache(whichByte)

	s.allocCache >>= s.freeindex % 64

	return s
}

跨度类

runtime.spanClass 是 runtime.mspan 的跨度类,它决定了内存管理单元中存储的对象大小和个数:

type mspan struct {
	...
	spanclass   spanClass
	...
}

Go 语言的内存管理模块中一共包含 67 种跨度类,每一个跨度类都会存储特定大小的对象并且包含特定数量的页数以及对象,所有的数据都会被预选计算好并存储在 runtime.class_to_size 和 runtime.class_to_allocnpages 等变量中:

class	bytes/obj	bytes/span	objects	tail waste	max waste
1	8	8192	1024	0	87.50%
2	16	8192	512	0	43.75%
3	24	8192	341	0	29.24%
4	32	8192	256	0	46.88%
5	48	8192	170	32	31.52%
6	64	8192	128	0	23.44%
7	80	8192	102	32	19.07%
…	…	…	…	…	…
67	32768	32768	1	0	12.50%

上表展示了对象大小从 8B 到 32KB,总共 67 种跨度类的大小、存储的对象数以及浪费的内存空间,以表中的第四个跨度类为例,跨度类为 5 的 runtime.mspan 中对象的大小上限为 48 字节、管理 1 个页、最多可以存储 170 个对象。因为内存需要按照页进行管理,所以在尾部会浪费 32 字节的内存,当页中存储的对象都是 33 字节时,最多会浪费 31.52% 的资源:

((48−33)∗170+32)/8192=0.31518

中心缓存扩容

扩容方法 runtime.mcentral.grow

会根据预先计算的 class_to_allocnpages 和 class_to_size 获取待分配的页数以及跨度类并调用 runtime.mheap.alloc 获取新的 runtime.mspan 结构:

func (c *mcentral) grow() *mspan {
	npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
	size := uintptr(class_to_size[c.spanclass.sizeclass()])

	s := mheap_.alloc(npages, c.spanclass, true)
	if s == nil {
		return nil
	}

	n := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2
	s.limit = s.base() + size*n
	heapBitsForAddr(s.base()).initSpan(s)
	return s
}

获取了 runtime.mspan 后,我们会在上述方法中初始化 limit 字段并清除该结构在堆上对应的位图。

页堆与中心缓存列表

我们在设计原理一节中已经介绍过 Go 语言所有的内存空间都由如下所示的二维矩阵 runtime.heapArena 管理,这个二维矩阵管理的内存可以是不连续的:

[mheap]
[[heaparena],[heaparena],[heaparena],[heaparena],[heaparena]...]
每个heaparena对应一页

在除了 Windows 以外的 64 位操作系统中,每一个 runtime.heapArena 都会管理 64MB 的内存空间,如下所示的表格展示了不同平台上 Go 语言程序管理的堆区大小以及 runtime.heapArena 占用的内存空间:

平台 地址位数 Arena 大小 一维大小 二维大小

*/64-bit	48	64MB	1	4M (32MB)
windows/64-bit	48	4MB	64	1M (8MB)
*/32-bit	32	4MB	1	1024 (4KB)
*/mips(le)	31	4MB	1	512 (2KB)

本节将介绍页堆的初始化、内存分配以及内存管理单元分配的过程,这些过程能够帮助我们理解全局变量页堆与其他组件的关系以及它管理内存的方式。

评论 (2)