Go的内存分配

Go内存分配

一. 背景介绍

先了解一下Linux系统内存相关的背景知识,有助于我们了解Go的内存分配

1.覆盖技术

在上古时代的内存管理中,如果程序太大,超过了空闲内存容量,就无法把全部程序装入内存中,这个时候诞生出了一种解决方案,即覆盖技术,
简而言之,就是把程序分为若干个块,只把哪些需要用到的指令和数据载入内存中,但是这种技术存在一个很严重的问题,必须由程序员手动的给程序划分块,并确定各个块之间的调用关系

2.虚拟内存技术

覆盖技术这种方法,非常耗时,而且使得编程的复杂度大大提升,这个时候就又诞生出了一种解决方案,即虚拟内存技术

2.1 虚拟内存技术的原理

即虚拟内存是对内存的一种抽象,有了这层抽象之后,程序运行进程的总大小可以超过实际可用的物理内存大小,每个进程都有自己的独立虚拟地址空间,然后通过CPU和MMU把虚拟内存地址转换为实际物理地址

2.2 虚拟内存技术的分层设计

虚拟内存体系其实是一种分层设计,总共分为四层

  • 虚拟内存
  • 内存映射
  • 物理内存
  • 磁盘

Go的内存分配

进程访问虚拟内存的流程:进程访问内存,其实访问的是虚拟内存,虚拟内存通过内存映射查看当前需要访问的虚拟内存是否已经加载到了物理内存,
如果已经加载到了物理内存,则取物理内存的数据,
如果没有加载到物理内存,则从磁盘加载数据到物理内存,并且把物理内存地址和虚拟内存地址更新到内存映射表中

总结

在没有虚拟内存的远古时代,物理内存对所有进程都是共享的,多进程同时访问同一块物理内存需要加锁,锁的粒度是进程级别的,
在引入虚拟内存后,每个进程都有各自的虚拟内存,这个时候是多线程访问同一个物理内存需要加锁,锁的粒度是线程级别的,
可以看到,一步步锁的粒度的降低。其实在Go的内存分配中也是这种思想:降低内存并发访问的粒度。

二.TCMalloc

1.简单介绍

  • TCMalloc全称是Thread Cache Malloc,是google为C语言开发的内存分配算法,是Go内存分配的起源。
  • 在Linux中,存在很多的内存管理库,其本质都是在多线程下,追求更高的内存管理效率,更快的分配内存,TCMalloc同样也是如此。

2.核心思想

  • 其核心思想:把内存分为多级管理,从而降低锁的粒度,它将可用的堆内存采用二级分配的方式进行管理,每个线程都会自行维护一个独立的线程内存池,进行内存分配时优先从该线程内存池中分配,
    当线程内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争 ,进一步的降低了内存并发访问的粒度。

简单介绍一下TCMalloc中几个重要概念

  • Page: 操作系统对内存的管理同样是以页为单位,但TCMalloc中的Page和操作系统的中页是倍数关系,x64下Page大小为8KB
  • Span: 一组连续的Page被叫做Span,是TCMalloc内存管理的基本单位,有不同大小的Span,比如2个Page大的Span,16个Page大的Span
  • ThreadCache: 每个线程各自的Cache,每个ThreadCache包含多个不同规格的Span链表,叫做SpanList,
    内存分配的时候,可以根据要分配的内存大小,快速选择不同大小的SpanList,在SpanList上选择合适的Span,每个线程都有自己的ThreadCache,所以ThreadCache是无锁访问的
  • CentralCache: 中心Cache,所有线程共享的Cache,也是保存的SpanList,数量和ThreadCache中数量相同
    当ThreadCache中内存不足时,可以从CentralCache中获取
    当ThreadCache中内存太多时,可以放回CentralCache
    由于CentralCache是线程共享的,所以它的访问需要加锁
  • PageHeap: 堆内存的抽象,同样当CentealCache中内存太多或太少时,都可从PageHeap中放回或获取,同样,PageHeap的访问也是需要加锁的

TCMalloc中对不同的对象会区分其大小,不同大小的对象其内存的分配流程也不一样

  • 小对象:0-256KB,分配流程:ThreadCache -> CentralCache -> PageHead, 大部分时候,ThreadCache缓存都是足够的,不需要去访问CentralCache和PageHead,不存在锁,不存在系统调用,分配效率非常高
  • 中对象:267KB-1M,分配流程:直接在PageHead中分配
  • 大对象:大于1M,分配流程: 直接在large span set(PageHead中一个特殊的地方,专门用于大对象分配)中选择合适数量的Page组成Span

Go的内存分配

三. Go的内存分配

Go的内存分配和TCMalloc非常类似,仅有少量地方不同

  • Page: 和TCMalloc中Page相同
  • Span: 和TCMalloc中Span相同
  • mcache: 和TCMalloc中不同之处在于,TCMalloc中是每个线程持,而Go中是每个P(processor,逻辑处理器,go的并发调度器GPM模型中概念)持有,在go程序中,当前最多有GOMAXPROCS个线程在用户态运行,
    所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache无锁访问,而go中线程的运行又是与P绑定的,把mcache交给P刚好
  • mcentral: 和TCMalloc中CentralCache大致相同,不同之处在于CentralCache是每个size的Span有一个链表,mcache是每个size的span有两个链表,这和mcache的内存申请有关,后面再做解释
  • mheap: 与TCMalloc中PageHeap大致相同,不同之处在于,mheap把span组织成了树结构,而不是链表,并且还是两棵树,利用空间换时间,同样也是为了内存的分配效率更快

Go的内存分配

go的内存分类不像TCMalloc那样分成大中小对象,其只分为小对象和大对象,但其小对象又细分了一个Tiny对象

  • 小对象: (mcache -> mcenttral -> mheap 不够就向右逐级申请)
    • Tiny对象:指大小在1byte到16byte之间并且不包含指针的对象
    • 其他小对象:大小在16byte到32KB之间的对象
  • 大对象:大于32KB的对象,在mheap中分配

再来关注一下go是如何释放内存的

go释放内存的函数是sysUnused,它的功能是给内核提供一个建议:这个内存地址区间的内存已不再使用,可以进行回收,但内核是否进行回收,什么时候回收,都取决于内核

四.总结

总结一下内存分配中用到的两个重要思想

  • 利用缓存:第一个好处是可以减少系统调用次数,第二个好处是可以降低锁的粒度,减少加锁次数(以上两点,都针对内存分配时)
  • 空间换时间,提升速度: 空间换时间是一种常用的性能优化思想,在Hash,Map,二叉树等数据结构中非常普遍,数据库索引,Redis等缓存数据库,都是这种思想
上一篇:Bellman-Ford算法


下一篇:最短路-Bellmm-ford算法