Go内存分配机制-TCMalloc

本文是《循序渐进go语言》第四篇-Go内存分配机制。
      golang的内存采用了TCMalloc 这种分配机制。go是在多种语言诞生N多年之后,才由几个大神开发的。每种语言都有自己的内存分配机制,那么作为后起之秀,go为什么选用TCMalloc呢?因为TCMalloc(Thread-Cache Malloc) 在内存分配上性能做的更好,而且对内存的利用率也有所提高。先简单说一下TCMalloc在这两方面是如何做到的?

  • 性能提升主要从两点来看,第一点线程内部小对象的使用不存在锁竞争,减少了竞争带来的性能开销; 第二点内存大对象的分配直接在堆上,并且采用了自旋锁,某个线程等待另一个线程释放锁的时候,不会像传统互斥锁一样由运行态转到休眠态,等待线程一会处于忙等待,减少了线程状态的切换。
  • 在内存利用率方面,区分了线程、central、heap三级,线程之间有一个共享内存池(central 区)。当线程内部内存不足时,会向central申请,当不再使用时,归还到central区。每个线程都可以向central区申请和归还,充分利用了内存。central区内存不足时,跟heap申请,空闲时归还给heap。

      说了分配性能跟内存利用率方面的优势,有没有对TCMalloc产生浓浓的兴趣?
      本文将从如下几个方面,将我整理的知识点梳理一下:

  • TCMalloc内存块以及管理组件
  • go初始化时,在内存这部分做了什么?
  • TCMalloc 分配过程
  • TCMalloc 回收过程
  • TCMalloc 释放

1 TCMalloc内存块以及管理组件

1.1 内存块

go中的有两种内存块:span和object。span面向内部管理(可以是一种按照大小将内存页进行组织的形式),object面向内存分配。

  • span:由多个地址连续的页组成的大块内存
  • object: 将span按照特定大小切分成多个小块,每个小块可存储一个对象。

分配对象时,大的对象直接从堆上分配,小的对象从 Span 中分配。


Go内存分配机制-TCMalloc_第1张图片
小对象分配.png

span跟page的关系:


Go内存分配机制-TCMalloc_第2张图片
page-span.png

看一下span的定义哈:既包含起始页跟页数,又有object链表

type mspan struct {
      next *mspan //双向链表
      prev *mspan
      start pageID //起始页号
      npages uintptr //页数
      freelist gclinkptr //待分配的object 链表。
}

1.2 管理组件

tcmalloc 分配器有三种管理组件:

  • cache: 每个运行期工作线程都会有一个cache
  • central:为所有cache提供切分好的后备span资源
  • heap:管理闲置span,需要时向操作系统申请新内存

heap中包含一系列不同sizeclass的central,每个central中包含了对应大小的span列表,cache中 以sizeclass为索引管理多个用于分配的span。


Go内存分配机制-TCMalloc_第3张图片
image.png

2 go初始化时,在内存这部分做了什么?

在初始化阶段,预留了一大段虚拟地址空间,分了三部分:

首先是用户内存分配区域,其大小决定了可分配用户内存的上限
其次有一个位图bitmap,其为每个对象提供4bit标记位,用于保留指针、GC标记等信息
最后还有一个页所属span指针数组。

go 内存初始化做的工作主要有初始化上面的结构,然后为他们保留地址空间,然后初始化heap的一些其他属性。在初始化heap的时候,创建了多个不同大小的central。

3 TCMalloc 分配过程

3.1 TCMalloc分配

过程不复杂,我们画个流程图。


Go内存分配机制-TCMalloc_第4张图片
分配.png

分析了整体的线程内存分配流程,那么当资源不足时如何扩张呢?
资源不足分两种,第一种是central不足,另一种是heap不足。下面我们从分配的角度,看一下这两种的流程是什么样子的?

3.2 central的分配

Go内存分配机制-TCMalloc_第5张图片
central分配.png

其中维持noneempty跟empty链表过程中涉及到了sweepgen,这个用于central 中span的清理。
从central 里获取span时,优先取用已有资源。哪怕是要执行清理操作,只有当现有资源不满足时才会去heap中获取span。

3.3 heap的分配

heap自己维护了两个链表,busy跟busylarge,顾名思义,就是按照大小区分的。
在分配时,当是小对象时,放在busy链表供central使用,大对象放在busylarge,放在busylarge链表。

  • 为了避免浪费,当返回更大的span时,会将多余部分切出来重新放回heap链表。
  • 同时,还会尝试合并相邻限制的span空间,减少碎片。

当heap中的内存不足时,就只能向操作系统申请了。

3.4 向操作系统申请分配

使用了mmap,从指定位置申请内存。需要同步扩张heap的 bitmap和spans区域,以及调整arena_used这个位置指示器。

3.5 总结一下分配过程
  • 1 计算待分配对象对应的size class
  • 2 从cache.alloc 找到对应规格相同的span
  • 3 从span.freelist链表提取可用的object,如果span.freelist没有span可用,执行4
  • 4 从central 获取新的span, 如果有,分配给cache使用;如果central也没有可用span,执行5
  • 5 从heap获取span。如果heap有,则将span切分成对应大小的object,并将整个span给到central;如果heap也不足,则执行6
  • 6 向操作系统申请。

4 TCMalloc 回收过程

Go内存分配机制-TCMalloc_第6张图片
回收.png

5 TCMalloc 释放

main启动时会启动一个监控任务sysmon,每隔一段时间就会检查heap中的闲置内存块,如果闲置时间超过阈值,则释放其关联的物理内存。

6 参考文献

1、官方文档
2、自旋锁
3、《go语言学习笔记》
4、图解TCMalloc

7 其他

本文是《循序渐进go语言》的第四篇-《Go内存分配机制-TCMalloc》。
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~

你可能感兴趣的:(Go内存分配机制-TCMalloc)