操作系统会给每个进程分配一定大小的虚拟内存空间,每个进程操作自己的虚拟内存空间,互不影响,虚拟内存到物理内存之间的映射交由操作系统来进行。
1.heapArena
- Go语言每次以heapArena为单位向虚拟内存申请内存空间,每次申请的内存单位为64MB
- 所有的heapArena组成了mheap(Go的堆内存)
- Go语言是一个一个内存块(heapArena)申请内存
2.Go的堆对象的分配内存
采用线性分配或者链表分配的性能不高并且会出现内存碎片,Go语言中采用了分级分配的策略。将一个heapArena中划分成许多大小相等的小格子,空间大小相同的格子划分为一个等级。
分级的思想: 为了防止内存碎片化,将heapArena划分成一组组的格子,如下图所示,相同的大小的各自属于一个组,这个组在Go语言的内存管理中叫做msapn,在为对象进行分配内存的时候,会找到一个能装下该对象的最小格子。
(1)mspan
N个相同的容量的格子组成一个mspan,一共有68(0-67,0级mspan比较特殊)种mspn每一种mspan中的格子的容量不一样。Go语言在一开始的时候并不是含有所有类型的mspan,而是按需分配的。例如,在我们的程序种只用到第一级的mspan和第8级的mspan,其他的用不到,则系统只会划分这两种类型的mspan,而不会划分别的其他的mspan。级别越大的mspan格子的容量就越大。
mspan的类型:
从左到右:类型、每个格子(span)能容纳的字节、一个该等级的mspan占用的字节数、一个该类型的mspan中格子(span)的个数、尾部的空间浪费、最大的空间浪费,最小排列
3.定位mspan
一个进程中可能会有多个heapArena,每个heapArena中含有多种maspan,如何快速定位一个新的对象要去的地方呢?如果采用遍历的方法,这样的性能开销是不可以接受的。采用索引的方法,提出了中央索引mcentral。
在mcentral一共含有136个索引,其中68个是需要GC扫描的,68个是不需要GC扫描的。在进行向对象中分配内存的时候,先根据对象的大小确定能装下该对象但是容量最小的span,也就是确定mspan的等级,然后去中心索引mcentral中找到对应的mspan,根据该对象是需要GC扫描的还是不需要的GC扫描的选择,空余的span。
注意:mcentral不是内存,只是一个索引(目录)。
mcentral的并发问题: 当多个对象向要分配一个同一个级别的内存的时候,在对一个对象进行查找中心索引,并对将对象放入到空余的span中的时候,需要修改mcentral的span的值,这时候就需要对修改的数据进行加锁。在高并发场景中可能会发生锁竞争问题,会导致性能下降。
4.解决mcentral锁竞争问题
参考GMP模型,增加本地缓存mcache,可以给每个线程分配一个本地队列P,本地队列中一共有136span(68个需要GC扫描,68个不需要GC扫描),这样每次在在分配内存的时候,可以在本地队列P中获取到最合适的span。换句话说就是,本地缓存mcache就是从中央索引中,每一种类型的span都拿出来一个空闲的span来,放在本地队列P中,然后我们需要分配内存的时候,先在本地的队列的136个span中找到最合适的span,这样就不需要mcentral中去找,然后为对象分配内存。
本地队列mcache中的某个span装满后,本地队列需要去中央索引(mcentral)中进行交换,从mcentral中交换一个空的同类型的span到本地队列中。
5.不同大小的对象内存分配
对象分类:
- 微对象0-16B,无指针
- 小对象16B-32KB
- 大对象大于32KB
微小对象分配至普通的mspan中,1-67级mspan。大对象需要定制mspan,0级mspan。
微对象:多个微对象合并成放在二级的span中(二级span16B)
小对象:直接找到相应级别的span