本文是《循序渐进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 中分配。
span跟page的关系:
看一下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。
2 go初始化时,在内存这部分做了什么?
在初始化阶段,预留了一大段虚拟地址空间,分了三部分:
首先是用户内存分配区域,其大小决定了可分配用户内存的上限
其次有一个位图bitmap,其为每个对象提供4bit标记位,用于保留指针、GC标记等信息
最后还有一个页所属span指针数组。
go 内存初始化做的工作主要有初始化上面的结构,然后为他们保留地址空间,然后初始化heap的一些其他属性。在初始化heap的时候,创建了多个不同大小的central。
3 TCMalloc 分配过程
3.1 TCMalloc分配
过程不复杂,我们画个流程图。
分析了整体的线程内存分配流程,那么当资源不足时如何扩张呢?
资源不足分两种,第一种是central不足,另一种是heap不足。下面我们从分配的角度,看一下这两种的流程是什么样子的?
3.2 central的分配
其中维持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 回收过程
5 TCMalloc 释放
main启动时会启动一个监控任务sysmon,每隔一段时间就会检查heap中的闲置内存块,如果闲置时间超过阈值,则释放其关联的物理内存。
6 参考文献
1、官方文档
2、自旋锁
3、《go语言学习笔记》
4、图解TCMalloc
7 其他
本文是《循序渐进go语言》的第四篇-《Go内存分配机制-TCMalloc》。
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~