操作系统会给每个进程分配一定大小的虚拟内存空间,每个进程操作自己的虚拟内存空间,互不影响,虚拟内存到物理内存之间的映射交由操作系统来进行。

1.heapArena

  • Go语言每次以heapArena为单位向虚拟内存申请内存空间,每次申请的内存单位为64MB
  • 所有的heapArena组成了mheap(Go的堆内存)
  • Go语言是一个一个内存块(heapArena)申请内存

go语言程序内存和cpu占比一直变大_虚拟内存


2.Go的堆对象的分配内存

采用线性分配或者链表分配的性能不高并且会出现内存碎片,Go语言中采用了分级分配的策略。将一个heapArena中划分成许多大小相等的小格子,空间大小相同的格子划分为一个等级。

分级的思想: 为了防止内存碎片化,将heapArena划分成一组组的格子,如下图所示,相同的大小的各自属于一个组,这个组在Go语言的内存管理中叫做msapn,在为对象进行分配内存的时候,会找到一个能装下该对象的最小格子

go语言程序内存和cpu占比一直变大_数据结构_02


(1)mspan

N个相同的容量的格子组成一个mspan,一共有68(0-67,0级mspan比较特殊)种mspn每一种mspan中的格子的容量不一样。Go语言在一开始的时候并不是含有所有类型的mspan,而是按需分配的。例如,在我们的程序种只用到第一级的mspan和第8级的mspan,其他的用不到,则系统只会划分这两种类型的mspan,而不会划分别的其他的mspan。级别越大的mspan格子的容量就越大。

go语言程序内存和cpu占比一直变大_虚拟内存_03


mspan的类型:

从左到右:类型、每个格子(span)能容纳的字节、一个该等级的mspan占用的字节数、一个该类型的mspan中格子(span)的个数、尾部的空间浪费、最大的空间浪费,最小排列

go语言程序内存和cpu占比一直变大_虚拟内存_04


go语言程序内存和cpu占比一直变大_go语言程序内存和cpu占比一直变大_05


3.定位mspan

一个进程中可能会有多个heapArena,每个heapArena中含有多种maspan,如何快速定位一个新的对象要去的地方呢?如果采用遍历的方法,这样的性能开销是不可以接受的。采用索引的方法,提出了中央索引mcentral。

go语言程序内存和cpu占比一直变大_Go_06

在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中去找,然后为对象分配内存。

go语言程序内存和cpu占比一直变大_虚拟内存_07

本地队列mcache中的某个span装满后,本地队列需要去中央索引(mcentral)中进行交换,从mcentral中交换一个空的同类型的span到本地队列中。

5.不同大小的对象内存分配

对象分类:

  1. 微对象0-16B,无指针
  2. 小对象16B-32KB
  3. 大对象大于32KB

微小对象分配至普通的mspan中,1-67级mspan。大对象需要定制mspan,0级mspan。
微对象:多个微对象合并成放在二级的span中(二级span16B)
小对象:直接找到相应级别的span