先了解一下Linux系统内存相关的背景知识,有助于我们了解Go的内存分配
在上古时代的内存管理中,如果程序太大,超过了空闲内存容量,就无法把全部程序装入内存中,这个时候诞生出了一种解决方案,即覆盖技术,
简而言之,就是把程序分为若干个块,只把哪些需要用到的指令和数据载入内存中,但是这种技术存在一个很严重的问题,必须由程序员手动的给程序划分块,并确定各个块之间的调用关系
覆盖技术这种方法,非常耗时,而且使得编程的复杂度大大提升,这个时候就又诞生出了一种解决方案,即虚拟内存技术
即虚拟内存是对内存的一种抽象,有了这层抽象之后,程序运行进程的总大小可以超过实际可用的物理内存大小,每个进程都有自己的独立虚拟地址空间,然后通过CPU和MMU把虚拟内存地址转换为实际物理地址
虚拟内存体系其实是一种分层设计,总共分为四层
进程访问虚拟内存的流程:进程访问内存,其实访问的是虚拟内存,虚拟内存通过内存映射查看当前需要访问的虚拟内存是否已经加载到了物理内存,
如果已经加载到了物理内存,则取物理内存的数据,
如果没有加载到物理内存,则从磁盘加载数据到物理内存,并且把物理内存地址和虚拟内存地址更新到内存映射表中
在没有虚拟内存的远古时代,物理内存对所有进程都是共享的,多进程同时访问同一块物理内存需要加锁,锁的粒度是进程级别的,
在引入虚拟内存后,每个进程都有各自的虚拟内存,这个时候是多线程访问同一个物理内存需要加锁,锁的粒度是线程级别的,
可以看到,一步步锁的粒度的降低。其实在Go的内存分配中也是这种思想:降低内存并发访问的粒度。
简单介绍一下TCMalloc中几个重要概念
Page
: 操作系统对内存的管理同样是以页为单位,但TCMalloc中的Page和操作系统的中页是倍数关系,x64下Page大小为8KBSpan
: 一组连续的Page被叫做Span,是TCMalloc内存管理的基本单位,有不同大小的Span,比如2个Page大的Span,16个Page大的SpanThreadCache
: 每个线程各自的Cache,每个ThreadCache包含多个不同规格的Span链表,叫做SpanList,CentralCache
: 中心Cache,所有线程共享的Cache,也是保存的SpanList,数量和ThreadCache中数量相同PageHeap
: 堆内存的抽象,同样当CentealCache中内存太多或太少时,都可从PageHeap中放回或获取,同样,PageHeap的访问也是需要加锁的TCMalloc中对不同的对象会区分其大小,不同大小的对象其内存的分配流程也不一样
Go的内存分配和TCMalloc非常类似,仅有少量地方不同
Page
: 和TCMalloc中Page相同Span
: 和TCMalloc中Span相同mcache
: 和TCMalloc中不同之处在于,TCMalloc中是每个线程持,而Go中是每个P(processor,逻辑处理器,go的并发调度器GPM模型中概念)持有,在go程序中,当前最多有GOMAXPROCS个线程在用户态运行,mcentral
: 和TCMalloc中CentralCache大致相同,不同之处在于CentralCache是每个size的Span有一个链表,mcache是每个size的span有两个链表,这和mcache的内存申请有关,后面再做解释mheap
: 与TCMalloc中PageHeap大致相同,不同之处在于,mheap把span组织成了树结构,而不是链表,并且还是两棵树,利用空间换时间,同样也是为了内存的分配效率更快go的内存分类不像TCMalloc那样分成大中小对象,其只分为小对象和大对象,但其小对象又细分了一个Tiny对象
再来关注一下go是如何释放内存的
go释放内存的函数是sysUnused,它的功能是给内核提供一个建议:这个内存地址区间的内存已不再使用,可以进行回收,但内核是否进行回收,什么时候回收,都取决于内核
总结一下内存分配中用到的两个重要思想