golang内存分配3
golang中实现了内存分配器,原理与tcmalloc类似。从内存申请一大块内存,通过内存分配器自己管理这块内存。
在64位系统中,go程序启动时会向系统申请512MB的spans 、16GB的的bitmap、512G的arena。
为了方便管理,arena区被划分成一个个页,每个页为8KB,一共有 512G/8K 个页 = 64*1024*1024
spans和bitmap区是为了管理arena区而存在的
spans区存放每个页的地址,共需要 64*1024*1024个地址,每个地址占8字节,需要512MB
bitmap区是根据arena计算出来的,主要用于GC。
数据结构
1. class
根据对象大小,划分了一系列class,每个class都代表一个固定大小的对象,以及每个span的大小
共有67种class,1-66种class,支持的最大对象为32K,超过32K大小由特殊的class表示,该class ID 为0.
class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型
bytes/obj:该class代表对象的字节数
bytes/span:每个span占用堆的字节数,也即页数*页大小
objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)
waste bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)
2. span是内存管理的基本单元
span是用于管理arena页的关键数据结构,每个span中包含1个或多个连续页,为了满足小对象分配,span中的每一页会划分更小的粒度,而对于大对象比如超过页大小的,则通过多页实现。
src/runtime/mheap.go 定义了mspan的数据结构,mspan的数据结构实际上是一个双向链表,同时包含了span的基础信息
type mspan struct{
next *mspan // 链表后向指针
prev *mspan // 链表前向指针,用于将span链接起来
startAddr uintptr // 起始地址,即所管理页的地址
npages uintptr // 管理的页数
nelems uintptr // 块个数,即有多少各块可供分配
allocBits *gcBits // 分配位图,每一位代表一个块是否以及分配
allocCount uint16 // 已分配的个数
spanclass spanClass // class表的classID
elemsize uintptr // class表中的对象大小,即块大小
}
例如:class10 ,144bytes/obj ,8192bytes/span,56 objects,128 waste bytes
class10的mspan为:npages = 1 ,nelems = 56, spanclass=10,elemsize=144
3. cache
mcentral是为了管理span,各线程需要内存时 从mcentral管理的span中申请内存,为了避免多线程申请内存时不断的加锁,Golang为每个线程分配了span的缓存,这个缓存就是cache
src/runtime/mcache.go 定义了mcache的数据结构
type mcache struct{
alloc [67*2]*mspan // 按class分组的mspan列表
}
alloc为mspan的指针数组,数组大小为class总数的2倍。数组中每个元素代表了一种class类型的span列表,每种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指针,这么做是为了提高GC扫描性能,对于不包含指针的span列表,没必要去扫描。
根据对象是否包含指针,将对象分为noscan和scan两类,其中noscan代表没有指针,而scan则代表有指针,需要GC进行扫描。
mchache在初始化时是没有任何span的,在使用过程中会动态的从central中获取并缓存下来,跟据使用情况,每种class的span个数也不相同。上图所示,class 0的span数比class1的要多,说明本线程中分配的小对象要多一些。
对于mcache的作用理解:如果没有mcache,多个线程申请内存时,都是从mcentral管理的span中申请内存,那么需要对span进行加锁,有了mcache之后,当线程申请内存时,就把一整个span分配给mcache,线程从mcache的span中获取内存,避免了多线程操作同一个span造成需要加锁的情况。
对于noscan 和scan的理解。GC扫描时,当扫描到某个对象,发现它引用了其他对象,就把它标记为黑色,并把它引用的对象标记为灰色,继续扫描,如果这个对象没有引用别的对象,就把它标记为黑色不清理。基于这个原理,对于非指针类型的对象,标记为黑色,对于指针类型的对象,还需要继续扫描指针对象所引用的对象。区分noscan和scan,在GC时只扫描包含指针对象的scan区,不需要扫描noscan区,从而提高GC扫描性能。
4. central
cache作为线程的私有资源为单个线程服务,而central则是全局资源,为多个线程服务,当某个线程内存不足时会向central申请,当某个线程释放内存时又会回收进central。
src/runtime/mcentral.go定义了mcentral的数据结构
type mcentral struct{
lock mutex //线程间互斥锁,防止多线程读写冲突
spanclass spanclass // span class ID ,每个mcentral管理着一组有相同class的span列表
nonempty mspanList // non-empty 指还有空闲块的span列表,还有内存可用的span列表
empty spanList // 指没有空闲块的span列表,没有内存可用的span列表
nmalloc uint64 // 已累计分配的对象个数
}
线程从central获取span步骤如下:
- 加锁
- 从nonempty列表获取一个可用span,并将其从链表中删除
- 将取出的span放入empty链表
- 将span返回给线程
- 解锁
- 线程将该span缓存进cache
线程将span归还步骤如下:
- 加锁
- 将span从empty列表删除
- 将span加入noneempty列表
- 解锁
上述线程从central中获取span和归还span只是简单流程,为简单起见,并未对具体细节展开。
5. heap
因为每个mcentral对象只管理特定class规格的span,所以每种class都有一个对应的mcentral。而mcentral是存放在mheap中的
src/runtime/mheap.go
type mheap struct {
lock mutex // 互斥锁
spans []*mspan // 指向spans区域(低地址向高地址增长),用于映射span和page的关系
bitmap uintptr // 指向bitmap首地址,bitmap是从高地址向低地址增长的
arena_start uintptr // 指示arena区首地址,(低地址向高地址增长)
arena_used uintptr // 指示arena已使用区域的最大地址位置
central [67*2]struct{
mcentral mcentral
pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
} // 每种class对应两个mcentral,scan和noscan
}
从数据结构可见,mheap管理着全部的内存,事实上Golang就是通过mheap类型的全局变量进行内存管理的。
针对待分配对象的大小不同有不同的分配逻辑:
- (0, 16B) 且不包含指针的对象: Tiny分配
- (0, 16B) 包含指针的对象:正常分配
- [16B, 32KB] : 正常分配
- (32KB, -) : 大对象分配 其中Tiny分配和大对象分配都属于内存管理的优化范畴,这里暂时仅关注一般的分配方法。
总结
总结:在64位操作系统中,golang程序开始运行,go程序向操作系统申请512M+16G+512G的一个内存空间。
其中512M为spans区,16G为bitmap区,512G为arena区。其中512G的arena区被划分为一个个8KB大小的页。
spans区域存放span的指针,span是内存管理的基本单位,每个span最少管理一个页,加入每个span只管理一个页,需要512M的空间来存放span指针。bitmap区主要用于GC
数据结构:
为了更好的利用内存空间,减少内存碎片,根据对象大小分成67种class的对象类型
每个span包含一个或多个连续的页,span是一个双向链表的数据结构,每个span只能存放一种class类型的对象。span中的StartAddr为管理的起始页的地址
每个线程都有自己独享的cache,cache由67*2个span列表组成,每种class各有两个span列表,一种是有指针的scan列表,一种是没有指针的noscan列表。线程会动态地向central申请span,当线程需要申请内存时,central用来给cache分配span,每个central只能分配一种class类型地span,所以需要67*2个central。全局变量heap保存了centrals,bitmaps,spans的信息
当go程序启动时,go向操作系统申请内存并自己管理这部分内存,初始化mheap,当线程需要申请内存时
- 获取当前线程的私有缓存mcache
- 根据对象size计算出适合的class ID
- 从mcache中的calloc[class]中查找可用的span,mspan的allbits记录着哪些元素已经分配
- 如果mcache中没有可用的span,则从mcentral申请一个新的span加入mcache中
- 如果mcentral中页没有可用的span,则从mheap中申请一个新的span加入mcentral
- 从该span中获取空闲对象地址并返回
Spans
spans中保存的是mspan(是arena中分隔页组成的基本内存管理单元)的指针,每个指针对应一页(会出现多个s指向同一个mspan),每一个mspan中也保存了对应的spans在回收时可以快速的找到。
可以理解为spans的作用是表示每个页被分配到哪个mspan中了
spans不同于mspan ,首先mspan里记录的是页的首地址,一个mspan可以表示多个页。而spans是mspan的地址,spans的每一个s跟每个页一一对应,这样就会存在有几个页所对应的s指向的是同一个mspan的地址。
mcache:Go中为每个逻辑处理器P提供一个本地线程缓存即mcache,每个P同一时间只能运行一个goroutine,因此访问mcache是不需要加锁
mcahe初始化的时候是没有mspan资源的,在使用过程中动态地从mcentral中申请,然后缓存起来
mheap:
当mcahce中没有空闲的span时向mcentral申请,当mcentral中没有空闲的span时,mcentral向mheap申请,当mheap中没有空闲的资源时,向操作系统申请。
mheap的作用:大对象分配内存,管理未切割的mspan
内存分配策略:
- 对象大于32K时直接从mheap上分配
- <=16B 且不是指针对象,使用mcahce的tiny分配器分配
- 其他情况,正常流程分配
Tiny分配和大对象分配都属于内存管理的优化范畴
在golang里面内存分为部分,传统意义上的栈由 runtime 统一管理,用户态不感知。而传统意义上的堆内存,又被 Go 运行时划分为了两个部分,
- 一个是 Go 运行时自身所需的堆内存,即堆外内存;
- 另一部分则用于 Go 用户态代码所使用的堆内存,也叫做 Go 堆。
Go 堆负责了用户态对象的存放以及 goroutine 的执行栈。
资料
-
程序员菜刚 图解golang内存分配
-
https://blog.csdn.net/weixin_40108561/article/details/104735390
转载:https://blog.csdn.net/weixin_43513459/article/details/117431835