开始了解Go内存分配之前我们来简单了解下虚拟内存技术。
物理内存:实际通过物理内存而获得的内存空间
虚拟内存:与物理内存相反,是指根据系统需要从硬盘中虚拟的划出一部分存储空间
而虚拟内存技术就是对内存的一种抽象,有了这层抽象之后,程序运行进程的总大小可以超过实际可用的物理内存大小,每个进程都有自己的独立虚拟地址空间,然后通过CPU和MMU把虚拟内存地址转换为实际物理地址。
TCMalloc全称是Thread Cache Malloc,是google为C语言开发的内存分配算法,是Go内存分配的起源。我们对它做个简单的了解,看看它的核心思想和几个重要概念,更能帮助我们理解Go内存分配和TCMalloc的相似和不同的地方。
TCMalloc内存分配算法的核心思想是把内存分为多级管理,从而降低锁的粒度,它将可用的堆内存采用二级分配的方式进行管理,每个线程都会自行维护一个独立的线程内存池,进行内存分配时优先从该线程内存池中分配, 当线程内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争 ,进一步的降低了内存并发访问的粒度。
Go的内存分配算法是基于TCMalloc(Thread Cache malloc,线程缓存分配器)内存分配算法实现的,通过借鉴了TCmalloc的思想,开发出Go的内存分配器,核心实现在内置运行时(就是runtime)。
Go在程序启动的时候,会分配一块连续的内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理,对内存的分配遵循以下思想。
先看图,我们先在脑中构造一个基础的概念图,然后再一个个解释,我觉得这种方式比只读枯燥的文字更有效。
我们从Go内存管理结构图中可以看出内存管理由mcache、mcentral、mheap组成一个三级管理结构,本质上都是对mspan的管理,三者之间没有严格的包含关系,只是用于不同的目的来共同配合管理所有mspan。
mspan其实就是Go中内存管理的基本单元,是由一片连续的 8kB 的页(page)组成的内存块。小对象和大对象分配的位置不用,大对象在mheap上分配,小对象使用mcache的tiny分配器分配。而文章开始我们为什么要去了解虚拟内存技术呢,可以看到mheap向操作系统申请新内存时,是向虚拟内存申请。
Span是go内存管理的基本单位,代码中为mspan,一组连续的Page组成1个Span,所以上图一组连续的浅蓝色长方形代表的是一组Page组成的1个Span,另外,1个淡紫色长方形为1个Span。go把内存分为67个大小不同的span(SizeClass有67种),并且大小是不固定的。
sizeclasses.go对span数量67写死在代码中:
// src/runtime/mheap.go 的mspan结构体
type mSpanList struct {
first *mspan // first span in list, or nil if none
last *mspan // last span in list, or nil if none
}
type mspan struct {
next *mspan // 链表后向指针,用于将span链接起来
prev *mspan // 链表前向指针,用于将span链接起来
list *mSpanList // 双端队列的head
startAddr uintptr // 起始地址,也即所管理页的地址
npages uintptr // 块个数,表示有多少个块可供分配
...
}
内存管理器由mcache, mcentral, mheap3种组件构成: 三级管理结构是为了方便对span进行管理,加速对span对象的访问和分配,这三个结构在runtime中分别有对应的mcache.go、mcentral.go、mheap.go文件。对于如何实现申请、分配、释放内存的代码我们就不去做了解了,了解原理应付面试就够了。
把这些概念结合起来,可以用下面图进行概述三者之间的联系和对mspan的不同处理。
Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。
大体上的分配流程:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。