golang--内存管理器
作者:互联网
一.前言
笔者在经过了前期基础学习后,用go语言来实现自己面临的业务问题已经不再是问题,所以拥有了另一方面的求知欲--go语言自身的各种包,各种机制是如何实现的,本章主要在探究go语言的内存分配器,希望能用本文讲清楚go语言内存分配器的机制,帮助大家更好地理解go语言的运行机制。
二.简介
不同于c语言使用malloc和free来主动管理内存,golang让程序员避免了这一切繁杂的操作,它通过escape analysis来分配内存,通过garbage collection(gc)来回收内存,本文主要介绍内存分配部分的工作。
三.详细解释
3.1 golang内存分配时机
程序有两种内存,一种是堆栈(stack),一种是堆(heap),所有的堆内数据都被GC管理。
我们要明白什么时候程序会分配内存,在某些语言中是程序员主动申请的,在go语言中则依赖escape analysis,越多的值在堆栈,程序运行越快(存取速度比堆要快,仅次于直接位于CPU中的寄存器),以下是内存分配的一些时机
1. goloang只会把函数中确定不在函数结束后使用的变量放到堆栈,否则就会放到堆:一个值可能在构造该值的函数之后被引用-->变量上传
package main
func main(){
n:=answer()
println(*n/2)
}
func answer() *int{
x:=42
return &x
}
使用命令 go build -gcflags="-m -l"得到结果
./main.go:10:2: moved to heap: x
2.编译器确定值太大而无法放入堆栈
3.编译器在编译的时候无法得知这个值的具体大小
ps:将变量下传,变量还会留在堆栈中
type Reader interface{
Read(p []byte) (n int,err error)
}
//better than
type Reader interface{
Read(n int) (b []byte,err error)
}
//因为从上面传参数下去用的是堆栈,从下面往上传,则会escape到堆,导致程序更慢
3.2 golang内存分配方式
3.2.1 TCMalloc
学习go语言的内存分配方式之前,我们先来看看另一个内存分配器-->TCMalloc,全称Thread-Caching Malloc
。
TCMalloc有两个重要组成部分:线程内存(thread cache)和页堆(page heap)
3.2.1.1 线程内存
每一个内存页都被分为多个固定分配大小规格的空闲列表(free list
) 用于减少碎片化。这样每一个线程都可以获得一个用于无锁分配小对象的缓存,这样可以让并行程序分配小对象(<=32KB)非常高效。
如图所示,第一行就是长度为8字节的内存块,在thread cache内最大的为256字节的内存块
3.2.1.2 页堆
TCMalloc 管理的堆由一组页组成(page一般大小为4kb),一组连续的页面被表示为 span。当分配的对象大于 32KB,将使用页堆(Page Heap)进行内存分配,分配对象时,大的对象直接分配 Span,小的对象从 Span 中分配。
当没有足够的空间分配小对象则会到页堆获取内存。如果页堆页没有足够的内存,则页堆会向操作系统申请更多的内存。
3.2.1.3 内存分配器
将基于 Page 的对象分配,和Page本身的管理串联
每种规格的对象,都从不同的 Span 进行分配;每种规则的对象都有一个独立的内存分配单元:CentralCache。在一个CentralCache 内存,我们用链表把所有 Span 组织起来,每次需要分配时就找一个 Span 从中分配一个 Object;当没有空闲的 Span 时,就从 PageHeap 申请 Span。
3.2.1.3 总结
最终我们得到结构图如下:
TCMalloc针对不同的对象分配采用了不同的形式
每个线程都一个线程局部的 ThreadCache,按照不同的规格,维护了对象的链表;如果ThreadCache 的对象不够了,就从 CentralCache 进行批量分配;如果 CentralCache 依然没有,就从PageHeap申请Span;如果 PageHeap没有合适的 Page,就只能从操作系统申请了。
在释放内存的时候,ThreadCache依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache;CentralCache发现一个 Span的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap发现一批连续的Page都释放了,就可以归还给操作系统。
由此,TCMalloc的核心思路即:
把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。
3.2.2 Go内存分配器结构
3.2.2.1 初始化
Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。
申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB大小。
arena区域
就是我们所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成8KB
大小的页,一些页组合起来称为mspan
。
bitmap区域
标识arena
区域哪些地址保存了对象,并且用4bit
标志位表示对象是否包含指针、GC
标记信息。bitmap
中一个byte
大小的内存对应arena
区域中4个指针大小(指针大小为 8B )的内存,所以bitmap
区域的大小是512GB/(4*8B)=16GB
。
spans区域
存放mspan
(也就是一些arena
分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以spans
区域的大小就是512GB/8KB*8B=512MB
。除以8KB是计算arena
区域的页数,而最后乘以8是计算spans
区域所有指针的大小。创建mspan
的时候,按页填充对应的spans
区域,在回收object
时,根据地址很容易就能找到它所属的mspan
。
spans区域
存放mspan
(也就是一些arena
分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以spans
区域的大小就是512GB/8KB*8B=512MB
。除以8KB是计算arena
区域的页数,而最后乘以8是计算spans
区域所有指针的大小。创建mspan
的时候,按页填充对应的spans
区域,在回收object
时,根据地址很容易就能找到它所属的mspan
。
go初始化的时候会将内存页分为如下67个不同大小的内存块,最大到32kb
3.2.2.2 结构及流程总览
Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。
大体上的分配流程:
1.32KB 的对象,直接从mheap上分配;
2.<=16B 的对象使用mcache的tiny分配器分配;
3.(16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
4.如果mcache没有相应规格大小的mspan,则向mcentral申请
5.如果mcentral没有相应规格大小的mspan,则向mheap申请
6.如果mheap中也没有合适大小的mspan,则向操作系统申请
3.2.2.3 自底向上名词解释
3.2.2.3.1 内存管理单元
mspan:Go中内存管理的基本单元,是由一片连续的8KB的页组成的大块内存。是一个包含起始地址、mspan规格、页的数量等内容的双端链表,mspan由一组连续的页组成,按照一定大小划分成object
。
结构图
3.2.2.3.2 内存管理元件
mcache:Go 像 TCMalloc 一样为每一个 逻辑处理器(P)(Logical Processors) 提供一个本地线程缓存(Local Thread Cache)称作 mcache,所以如果 Goroutine 需要内存可以直接从 mcache 中获取,由于在同一时间只有一个 Goroutine 运行在 逻辑处理器(P)(Logical Processors) 上,所以中间不需要任何锁的参与。
对于每一种大小规格都有两个类型:
-
scan -- 包含指针的对象。
-
noscan -- 不包含指针的对象。
采用这种方法的好处之一就是进行垃圾回收时 noscan 对象无需进一步扫描是否引用其他活跃的对象。
(<=16B 的对象使用mcache的tiny分配器分配)
结构体
//path: /usr/local/go/src/runtime/mcache.go
type mcache struct {
alloc [numSpanClasses]*mspan
}
numSpanClasses = _NumSizeClasses << 1
结构图
central(mcentral):为所有mcache
提供切分好的mspan
资源。每个central
保存一种特定大小的全局mspan
列表,包括已分配出去的和未分配出去的。 每个mcentral
对应一种mspan
,而mspan
的种类导致它分割的object
大小不同。当工作线程的mcache
中没有合适(也就是特定大小的)的mspan
时就会从mcentral
获取。mcentral
被所有的工作线程共同享有,存在多个Goroutine竞争的情况,因此会消耗锁资源。
//path: /usr/local/go/src/runtime/mcentral.go
type mcentral struct {
// 互斥锁
lock mutex
// 规格
sizeclass int32
// 尚有空闲object的mspan链表
nonempty mSpanList
// 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
empty mSpanList
// 已累计分配的对象个数
nmalloc uint64
}
结构图
mheap:代表Go程序持有的所有堆空间,Go程序使用一个mheap
的全局对象_mheap
来管理堆内存。
当mcentral
没有空闲的mspan
时,会向mheap
申请。而mheap
没有资源时,会向操作系统申请新内存。mheap
主要用于大对象的内存分配,以及管理未切割的mspan
,用于给mcentral
切割成小对象。
同时我们也看到,mheap
中含有所有规格的mcentral
,所以,当一个mcache
从mcentral
申请mspan
时,只需要在独立的mcentral
中使用锁,并不会影响申请其他规格的mspan
。
//path: /usr/local/go/src/runtime/mheap.go
type mheap struct {
lock mutex
// spans: 指向mspans区域,用于映射mspan和page的关系
spans []*mspan
// 指向bitmap首地址,bitmap是从高地址向低地址增长的
bitmap uintptr
// 指示arena区首地址
arena_start uintptr
// 指示arena区已使用地址位置
arena_used uintptr
// 指示arena区末地址
arena_end uintptr
central [67*2]struct {
mcentral mcentral
pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}
}
结构图:
arena:golang中所有堆区的统称,以x64为例子就是512GB的虚拟地址空间。
四.之后目标
1.go语言的垃圾回收
2.进程调度,线程调度,协程调度
3.虚拟内存
五.参考学习
https://www.youtube.com/watch?v=ZMZpH4yT7M0
https://www.linuxzen.com/go-memory-allocator-visual-guide.html
https://zhuanlan.zhihu.com/p/29216091
https://zhuanlan.zhihu.com/p/59125443
标签:mspan,--,mcentral,golang,对象,内存,go,管理器,分配 来源: https://blog.csdn.net/hello_bravo_/article/details/119078657