go 内存分配

一、概述

(一)基本策略

  • 1、程序每次先从系统申请一大块内存(比如1MB),减少向系统申请内存频率,也就是说,先给我整块大的,以后少找你,不够了,再找你要一块大的;
  • 2、然后程序将大块内存,按照特定大小(后文将提到的sizeClass,单位可以理解为8字节)切分为小块(object),小块构成链表(span);
  • 3、为对象分配内存时,只需要按照对象的大小(sizeClass),从满足该大小的链表中,获取一小块即可;
  • 4、回收对象内存时,将该小块内存重新归还到原链表,一遍复用;
  • 5、如果闲置的很多,则会尝试归还部分内存给系统,降低程序整体开销
    注意上述回收,是指内存分配器的回收,内存分配器只管理内存块,并不关心对象状态,且不会主动回收内存。只有在垃圾回收器完成回收操作后,触发内存分配器回收内存

(二)内存分配器,将内存块分为两种。

  • span: 由地址连续的页(page)组成
  • object:将span按照特定大小的块切分成多个小块(object),每个小块用于对象存储。按8字节倍数分为N种

是不是有点懵,其实可以这么理解,就像上学时写作业

  • 程序向系统申请的一大块内存(比如1MB),这一大块内存,可以看成我们新买了的作业本;
  • 接着,制定一些书签,书签上需要标明sizeClass,当后续给不同大小的对象分配内存时,能够快速定位;
  • 作业本一些连续的页(page)就组成了span,然后对每页上的一行一行进行分块,可以理解为分object,但是需要根据书签(sizeClass)大小分块,比如书签为1的,我们就像书签下的这些页上的每一行分成一块object

(三)管理组件

Go起点高,直接采用tcMalloc(线程缓存内存)成熟架构
分配器由三种组件组成(组件去管理span,获取和释放object块):

  • cache: 每个运行工作线程都会绑定一个cache,用于无锁object分配
  • central:为所有cache提供切分好的后备span资源
  • heap:管理闲置span,需要时向系统申请或释放内存

(四)几个重要的结构体对象


_NUmsizeClasses = 67

type mspan struct {
    next *mspan  //双向链表,指向下一个span
    prev *mspan
    start pageID  //起始序号
    npages  uintptr //页数
    freelist  gclinkptr  //待分配的object 链表
}

type mcache struct {
	alloc [_NUmsizeClasses]*mspan   //以sizeclass为索引管理多个用于分配的span
}

type mcentral struct {
	szieclass int32  //规格大小
	noempty mspan   //链表,span中还有可用的空闲object
	empty mspan //链表,span中没有可用的空闲object
}

type mheap struct {
	free  [_MaxMHeapList]mspan   //页数在127以内的闲置 span 链表数组。因为每页大小是固定的,以page为索引,管理对应的span
	freelarge  msapn      //页数大于127 (大于1MB)span 大链表数组,大对象直接从heap分配回收
	central [_NUmsizeClasses]struct {
		mcentral mcentral
	}
}

分配器按照页数区分不同大小的span,比如 mheap ,以页数为单位将span 存放在管理数组中,需要时就以页数为索引进行查找。当然span大小并非
固定不变,在获取闲置的span时,如果没有找到合适大小的span,那就返回页数更多的span,此时引发裁剪操作,多余部分将构成新的span 被放回管理数组。分配器
还会尝试将相邻的空闲 span 合并,以构成更大的内存块,减少碎片,实现更灵活的分配策略。

用于存储的object,按照8字节倍数分为N种。比如说,大小为24字节的object可以用来存储范围为17——24字节的对象。虽然会有一些浪费,但是分配器只需要面对有限几种规格(sizeclass)的小块内存,优化了分配和复用策略。
同时,分配器会尝试将多个微小的对象组合到一个object内存块,以节约内存

分配器初始化时,会构建对照表,存储大小和规格的对应关系,包括用来切分的span页数(一页8KB)。
若对象大小超出特定的阈值(32KB),会被当做大对象特殊处理。

(五)分配流程

  • 1、计算待分配对象对应规格(sizeClass),也就是要几个8字节;
  • 2、从cache.alloc 数组中找到对应规格的span;
  • 3、从span.freelsit 链表中获取可用的object;
  • 4、若没有可用的,即span.freelsit为空,从central获取span
  • 5、如central.noempty为空,从heap.free 或者 heap.freelarge 中获取span,并切分object 链表;
  • 6、若还是没有找到大小合适且空闲的span,则向操作系统申请新内存块

(六)释放流程

  • 1、将标记为可回收的object交给所属span.freelist
  • 2、该span被放回central,也就是拼接至mcentral.nonempty链表后,但是不要以为mcache.alloc 数组中就没有该span,
    该span还在,任然保持对span的指针引用;
  • 3、如果span收回了所有的object,则将其还给heap,即mheap.freelist,以便重新分割复用;
  • 4、定期扫描heap长时间闲置的span,释放其占用的内存,也就是还给系统

注意,以上不包含大对象,他直接从heap分配和回收
作为工作线程私有且不被共享的cache是实现高性能无锁分配的关键,而central的作用是在多个cache间提高object利用率,避免内存浪费,将span归还heap,是为了在不同规格object需求间平衡。
在计算机科学里,没有什么问题是不能通过中间过程解决的,所以很多架构,都会有中间件这个存在。

假如cache获取span的一部分object后,那么该span中还有许多剩余的object,但是回收操作将该span交还给central,该span还可以给其他线程cache1,cache2...使用,cache并没有持有span,只是用span中object。
而归还到heap的过程,则可以这部分内存,被其他不同规格大小需求使用,重新切分。

posted @ 2020-09-29 17:46  fanzou  阅读(387)  评论(0编辑  收藏  举报