Golang内存分配
2020-12-08 09:06:32 38 举报
AI智能生成
Golang内存分配
作者其他创作
大纲/内容
TCMalloc
TCMalloc架构图
Front-end:
它是一个内存缓存,提供了快速分配和重分配内存给应用的功能。它主要有2部分组成:Per-thread cache 和 Per-CPU cache
它是一个内存缓存,提供了快速分配和重分配内存给应用的功能。它主要有2部分组成:Per-thread cache 和 Per-CPU cache
Middle-end:
职责是给Front-end提供缓存。也就是说当Front-end缓存内存不够用时,从Middle-end申请内存。它主要是 Central free list 这部分内容。
职责是给Front-end提供缓存。也就是说当Front-end缓存内存不够用时,从Middle-end申请内存。它主要是 Central free list 这部分内容。
Back-end:
这一块是负责从操作系统获取内存,并给Middle-end提供缓存使用。它主要涉及 Page Heap 内容。
这一块是负责从操作系统获取内存,并给Middle-end提供缓存使用。它主要涉及 Page Heap 内容。
概念
Page
操作系统对内存管理的单位,TCMalloc也是以页为单位管理内存,但是TCMalloc中Page大小是操作系统中页的倍数关系。2,4,8 ....
Span
Span 是PageHeap中管理内存页的单位,它是由一组连续的Page组成,比如2个Page组成的span,多个这样的span就用链表来管理。当然,还可以有4个Page组成的span等等。
ThreadCache
ThreadCache是每个线程各自独立拥有的cache,一个cache包含多个空闲内存链表(size classes),每一个链表(size-class)都有自己的object,每个object都是大小相同的。
CentralCache
CentralCache是当ThreadCache内存不足时,提供内存供其使用。它保持的是空闲块链表,链表数量和ThreadCache数量相同。ThreadCache中内存过多时,可以放回CentralCache中。
PageHeap
PageHeap保存的也是若干链表,不过链表保存的是Span(多个相同的page组成一个Span)。CentralCache内存不足时,可以从PageHeap获取Span,然后把Span切割成object
小对象内存分配 ThreadCache
ThreadCache内存分配
大对象内存分配 PageHeap
PageHeap负责向操作系统申请内存。
tcmalloc也是基于页的分配方式,即每次申请至少一页(page)的内存大小。tcmalloc中一页大小为8KB(默认,可设置),多数linux中一页为4KB,tcmallo的一页是linux一页大小的2倍。
tcmalloc也是基于页的分配方式,即每次申请至少一页(page)的内存大小。tcmalloc中一页大小为8KB(默认,可设置),多数linux中一页为4KB,tcmallo的一页是linux一页大小的2倍。
Span
Span List
Page Heap管理Span
Middle end-Central Free List
CentralFreeList是CentralCahe中,它的作用就是从PageHeap中取出部分Span,然后按照预定大小将其拆分成固定大小的object,提供给ThreadCache使用
Golang中的内存分配器
内存组件
Golang内存分配架构
基础概念
mspan
mspan跟tcmalloc中的span相似,它是golang内存管理中的基本单位,也是由页组成的,每个页大小为8KB,与tcmalloc中span组成的默认基本内存单位页大小相同。
mspan里面按照8*2n大小(8b,16b,32b .... ),每一个mspan又分为多个object。
mspan里面按照8*2n大小(8b,16b,32b .... ),每一个mspan又分为多个object。
结构示意图
结构示意图
内存分配示意图
跨度类
runtime.spanClass 是 runtime.mspan 结构体的跨度类,它决定了内存管理单元中存储的对象大小和个数
跨度类对于内存布局
mcache
线程缓存
mcache跟tcmalloc中的ThreadCache相似,ThreadCache为每个线程的cache,同理,mcache可以为golang中每个Processor提供内存cache使用,每一个mcache的组成单位也是mspan
初始化
运行时在初始化处理器时会调用 runtime.allocmcache 初始化线程缓存,该函数会在系统栈中使用 runtime.mheap 中的线程缓存分配器初始化新的 runtime.mcache 结构体
替换
运行时在初始化处理器时会调用 runtime.allocmcache 初始化线程缓存,该函数会在系统栈中使用 runtime.mheap 中的线程缓存分配器初始化新的 runtime.mcache 结构体
微分配器
mcentral
中心缓存
mcentral跟tcmalloc中的CentralCache相似,当mcache中空间不够用,可以向mcentral申请内存。可以理解为mcentral为mcache的一个“缓存库”,供mcaceh使用。它的内存组成单位也是mspan。
mcentral里有两个双向链表,一个链表表示还有空闲的mspan待分配,一个表示链表里的mspan都被分配了。
mcentral里有两个双向链表,一个链表表示还有空闲的mspan待分配,一个表示链表里的mspan都被分配了。
内存管理单元
线程缓存会通过中心缓存的 runtime.mcentral.cacheSpan 方法获取新的内存管理单元,该方法的实现比较复杂,我们可以将其分成以下几个部分:
从有空闲对象的 runtime.mspan 链表中查找可以使用的内存管理单元;
当内存单元等待回收时,将其插入 empty 链表、调用 runtime.mspan.sweep 清理该单元并返回
当内存单元正在被后台回收时,跳过该内存单元;
当内存单元已经被回收时,将内存单元插入 empty 链表并返回;
从没有空闲对象的 runtime.mspan 链表中查找可以使用的内存管理单元;
如果中心缓存没有在 nonempty 中找到可用的内存管理单元,就会继续遍历其持有的 empty 链表,我们在这里的处理与包含空闲对象的链表几乎完全相同。当找到需要回收的内存单元时,我们也会触发 runtime.mspan.sweep 进行清理,如果清理后的内存单元仍然不包含空闲对象,就会重新执行相应的代码:
调用 runtime.mcentral.grow 从堆中申请新的内存管理单元;
更新内存管理单元的 allocCache 等字段帮助快速分配内存;
扩容
中心缓存的扩容方法 runtime.mcentral.grow 会根据预先计算的 class_to_allocnpages 和 class_to_size 获取待分配的页数
以及跨度类并调用 runtime.mheap.alloc 获取新的 runtime.mspan 结构
以及跨度类并调用 runtime.mheap.alloc 获取新的 runtime.mspan 结构
mheap
mheap跟tcmalloc中的PageHeap相似,负责大内存的分配。当mcentral内存不够时,可以向mheap申请。那mheap没有内存资源呢?跟tcmalloc一样,向OS操作系统申请。
还有,大于32KB的内存,也是直接向mheap申请。
还有,大于32KB的内存,也是直接向mheap申请。
runtime.mheap 是内存分配的核心结构体,Go 语言程序只会存在一个全局的结构,而堆上初始化的所有对象都由该结构体统一管理,该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段。
页堆中包含一个长度为 134 的 runtime.mcentral 数组,其中 67 个为跨度类需要 scan 的中心缓存,另外的 67 个是 noscan 的中心缓存:
页堆中包含一个长度为 134 的 runtime.mcentral 数组,其中 67 个为跨度类需要 scan 的中心缓存,另外的 67 个是 noscan 的中心缓存:
页堆
初始化
堆区的初始化会使用 runtime.mheap.init 方法,我们能看到该方法初始化了非常多的结构体和字段,不过其中初始化的两类变量比较重要:
spanalloc、cachealloc 以及 arenaHintAlloc 等 runtime.fixalloc 类型的空闲链表分配器;
central 切片中 runtime.mcentral 类型的中心缓存;
内存管理单元
runtime.mheap 是内存分配器中的核心组件,运行时会通过它的 runtime.mheap.alloc 方法在系统栈中获取新的 runtime.mspan
为了阻止内存的大量占用和堆的增长,我们在分配对应页数的内存前需要先调用 runtime.mheap.reclaim 方法回收一部分内存,接下来我们将通过 runtime.mheap.allocSpan 分配新的内存管理单元,我们会将该方法的执行过程拆分成两个部分
从堆上分配新的内存页和内存管理单元 runtime.mspan;
如果申请的内存比较小,获取申请内存的处理器并尝试调用 runtime.pageCache.alloc 获取内存区域的基地址和大小;
如果申请的内存比较大或者线程的页缓存中内存不足,会通过 runtime.pageAlloc.alloc 在页堆上申请内存;
如果发现页堆上的内存不足,会尝试通过 runtime.mheap.grow 进行扩容并重新调用 runtime.pageAlloc.alloc 申请内存;
如果申请到内存,意味着扩容成功;
如果没有申请到内存,意味着扩容失败,宿主机可能不存在空闲内存,运行时会直接中止当前程序
初始化内存管理单元并将其加入 runtime.mheap 持有内存单元列表;
我们通过调用 runtime.mspan.init 方法以及设置参数初始化刚刚分配的 runtime.mspan 结构并通过 runtime.mheaps.setSpans 方法建立页堆与内存单元的联系
扩容
runtime.mheap.grow 方法会向操作系统申请更多的内存空间,传入的页数经过对齐可以得到期望的内存大小,我们可以将该方法的执行过程分成以下几个部分
通过传入的页数获取期望分配的内存空间大小以及内存的基地址;
如果 arena 区域没有足够的空间,调用 runtime.mheap.sysAlloc 从操作系统中申请更多的内存
扩容 runtime.mheap 持有的 arena 区域并更新页分配器的元信息;
在某些场景下,调用 runtime.pageAlloc.scavenge 回收不再使用的空闲内存页;
内存分配
堆上所有的对象都会通过调用 runtime.newobject 函数分配内存,该函数会调用 runtime.mallocgc 分配指定大小的内存空间,这也是用户程序向堆上申请内存空间的必经函数
对象分配
微对象
0B - 16B
微对象
小对象
16B - 32K
确定分配对象的大小以及跨度类 runtime.spanClass
从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间;
调用 runtime.memclrNoHeapPointers 清空空闲内存中的所有数据;
大对象
32K - 无限
运行时对于大于 32KB 的大对象会单独处理,我们不会从线程缓存或者中心缓存中获取内存管理单元,而是直接在系统的栈中调用 runtime.largeAlloc 函数分配大片的内存
runtime.largeAlloc 函数会计算分配该对象所需要的页数,它会按照 8KB 的倍数为对象在堆上申请内存:
申请内存时会创建一个跨度类为 0 的 runtime.spanClass 并调用 runtime.mheap.alloc 分配一个管理对应内存的管理单元
内存布局
所有的 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。
设计原理
核心思想
分级分配:利用多级缓存将对象根据大小分类,按照类别实施不同的分配策略
分配方法
线性分配器
空闲链表分配器
隔离适应— 将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再从链表中选择合适的内存块;
虚拟内存布局
Go 1.10以前
1.10以前内存布局
spans 区域存储了指向内存管理单元 runtime.mspan 的指针,每个内存单元会管理几页的内存空间,每页大小为 8KB;
bitmap 用于标识 arena 区域中的那些地址保存了对象,位图中的每个字节都会表示堆区中的 32 字节是否包含空闲;
arena 区域是真正的堆区,运行时会将 8KB 看做一页,这些内存页中存储了所有在堆上初始化的对象;
对于任意一个地址,我们都可以根据 arena 的基地址计算该地址所在的页数并通过 spans 数组获得管理该片内存的管理单元 runtime.mspan,spans 数组中多个连续的位置可能对应同一个 runtime.mspan。
缺陷:
C与Go混用
C与Go混用
分配的内存地址会发生冲突,导致堆的初始化和扩容失败3;
没有被预留的大块内存可能会被分配给 C 语言的二进制,导致扩容后的堆不连续4;
线性的堆内存需要预留大块的内存空间,但是申请大块的内存空间而不使用是不切实际的,不预留内存空间却会在特殊场景下造成程序崩溃。虽然连续内存的实现比较简单,但是这些问题我们也没有办法忽略。
Go 1.10以后
稀疏内存
稀疏内存布局
runtime.heapArena
每个单元都会管理 64MB 的内存空间
该结构体中的 bitmap 和 spans 与线性内存中的 bitmap 和 spans 区域一一对应
zeroedBase字段指向了该结构体管理的内存的基地址。这种设计将原有的连续大内存切分成稀疏的小内存,而用于管理这些内存的元信息也被切分成了小块。
地址空间
状态
None
内存没有被保留或者映射,是地址空间的默认状态
Reserved
运行时持有该地址空间,但是访问该内存会导致错误
Prepared
内存被保留,一般没有对应的物理内存访问该片内存的行为,是未定义的可以快速转换到 Ready 状态
Ready
可以被安全访问
状态表
状态转换
runtime.sysAlloc 会从操作系统中获取一大块可用的内存空间,可能为几百 KB 或者几 MB;
runtime.sysFree 会在程序发生内存不足(Out-of Memory,OOM)时调用并无条件地返回内存;
runtime.sysReserve 会保留操作系统中的一片内存区域,对这片内存的访问会触发异常;
runtime.sysMap 保证内存区域可以快速转换至准备就绪;
runtime.sysUsed 通知操作系统应用程序需要使用该内存区域,需要保证内存区域可以安全访问;
runtime.sysUnused 通知操作系统虚拟内存对应的物理内存已经不再需要了,它可以重用物理内存;
runtime.sysFault 将内存区域转换成保留状态,主要用于运行时的调试;
0 条评论
下一页