Go语言内置运行时(就是runtime),抛弃了传统的内存分配方式,改为自主管理。这样可以自主地实现更好的内存使用模式,比如内存池、预分配等等。这样,不会每次内存分配都需要进行系统调用。
Golang运行时的内存分配算法主要源自 Google 为 C 语言开发的 TCMalloc算法 ,全称 Thread-CachingMalloc 。核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。
为了更好的阅读体验,手动贴上文章目录:
基础概念
Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。
申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB大小。
arena区域 就是我们所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成 8KB 大小的页,一些页组合起来称为 mspan 。
bitmap区域 标识 arena 区域哪些地址保存了对象,并用 4bit 标志位表示对象是否包含指针、 GC 标记信息。 bitmap 中一个 byte 大小的内存对应 arena 区域中4个指针大小(指针大小为 8B )的内存,所以 bitmap 区域的大小是 512GB/(4*8B)=16GB 。如下图:
从上图其实还可以看到bitmap的高地址部分指向arena区域的低地址部分,也就是说bitmap的地址是由高地址向低地址增长的。
spans区域 存放 mspan (也就是一些 arena 分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以 spans 区域的大小就是 512GB/8KB*8B=512MB 。除以8KB是计算 arena 区域的页数,而最后乘以8是计算 spans 区域所有指针的大小。创建 mspan 的时候,按页填充对应的 spans 区域,在回收 object 时,根据地址很容易就能找到它所属的 mspan 。
内存管理单元
mspan :Go中内存管理的基本单元,是由一片连续的 8KB 的页组成的大块内存。注意,这里的页和操作系统本身的页并不是一回事,它一般是操作系统页大小的几倍。一句话概括: mspan 是一个包含起始地址、 mspan 规格、页的数量等内容的双端链表。
每个 mspan 按照它自身的属性 SizeClass 的大小分割成若干个 object ,每个 object 可存储一个对象。并且会使用一个位图来标记其尚未使用的 object 。属性 SizeClass 决定 object 大小,而 mspan 只会分配给和 object 尺寸大小接近的对象,当然,对象的大小要小于 object 大小。还有一个概念: SpanClass ,它和 SizeClass 的含义差不多,
-
Size_Class = Span_Class / 2
这是因为其实每个 SizeClass 有两个 mspan ,也就是有两个 SpanClass 。其中一个分配给含有指针的对象,另一个分配给不含有指针的对象。这会给垃圾回收机制带来利好,之后的文章再谈。
如下图, mspan 由一组连续的页组成,按照一定大小划分成 object 。
Go1.9.2里 mspan 的 SizeClass 共有67种,每种 mspan 分割的object大小是8*2n的倍数,这个是写死在代码里的:
-
// path: /usr/local/go/src/runtime/sizeclasses.go
-
const _NumSizeClasses = 67
-
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
根据 mspan 的 SizeClass 可以得到它划分的 object 大小。 比如 SizeClass 等于3, object 大小就是32B。 32B大小的object可以存储对象大小范围在17B~32B的对象。而对于微小对象(小于16B),分配器会将其进行合并,将几个对象分配到同一个 object 中。
数组里最大的数是32768,也就是32KB,超过此大小就是大对象了,它会被特别对待,这个稍后会再介绍。顺便提一句,类型 SizeClass 为0表示大对象,它实际上直接由堆内存分配,而小对象都要通过 mspan 来分配。
对于mspan来说,它的 SizeClass 会决定它所能分到的页数,这也是写死在代码里的:
-
// path: /usr/local/go/src/runtime/sizeclasses.go
-
const _NumSizeClasses = 67
-
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
比如当我们要申请一个 object 大小为 32B 的 mspan 的时候,在classtosize里对应的索引是3,而索引3在 class_to_allocnpages 数组里对应的页数就是1。
mspan 结构体定义:
-
// path: /usr/local/go/src/runtime/mheap.go
-
type mspan struct {
-
//链表后向指针,用于将span链接起来
-
next *mspan
-
//链表前向指针,用于将span链接起来
-
prev *mspan
-
// 起始地址,也即所管理页的地址
-
startAddr uintptr
-
// 管理的页数
-
npages uintptr
-
// 块个数,表示有多少个块可供分配
-
nelems uintptr
-
//分配位图,每一位代表一个块是否已分配
-
allocBits *gcBits
-
// 已分配块的个数
-
allocCount uint16
-
// class表中的class ID,和Size Classs相关
-
spanclass spanClass
-
// class表中的对象大小,也即块大小
-
elemsize uintptr
-
}
我们将 mspan 放到更大的视角来看:
上图可以看到有两个 S |
请发表评论