飞道的博客

golang内存分配(三)

334人阅读  评论(0)

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步骤如下:

  1. 加锁
  2. 从nonempty列表获取一个可用span,并将其从链表中删除
  3. 将取出的span放入empty链表
  4. 将span返回给线程
  5. 解锁
  6. 线程将该span缓存进cache

线程将span归还步骤如下:

  1. 加锁
  2. 将span从empty列表删除
  3. 将span加入noneempty列表
  4. 解锁

上述线程从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,当线程需要申请内存时

  1. 获取当前线程的私有缓存mcache
  2. 根据对象size计算出适合的class ID
  3. 从mcache中的calloc[class]中查找可用的span,mspan的allbits记录着哪些元素已经分配
  4. 如果mcache中没有可用的span,则从mcentral申请一个新的span加入mcache中
  5. 如果mcentral中页没有可用的span,则从mheap中申请一个新的span加入mcentral
  6. 从该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

内存分配策略:

  1. 对象大于32K时直接从mheap上分配
  2. <=16B 且不是指针对象,使用mcahce的tiny分配器分配
  3. 其他情况,正常流程分配

Tiny分配和大对象分配都属于内存管理的优化范畴

在golang里面内存分为部分,传统意义上的栈由 runtime 统一管理,用户态不感知。而传统意义上的堆内存,又被 Go 运行时划分为了两个部分,

  • 一个是 Go 运行时自身所需的堆内存,即堆外内存;
  • 另一部分则用于 Go 用户态代码所使用的堆内存,也叫做 Go 堆。

Go 堆负责了用户态对象的存放以及 goroutine 的执行栈。

资料


转载:https://blog.csdn.net/weixin_43513459/article/details/117431835
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场