golang内存管理

176 阅读4分钟

golang的内存管理是非常复杂的,但是做了巧妙的设计,如果不了解其原理的话也能很好进行go程序开发,但是作为一个gopher还是有必要对这个系统做一个全面的了解

如果想要全面了解内存管理,应该从GMP调度GC逃逸分析、内存管理模型的顺序开始了解,然后再将其串在一起

go内存结构

golang内存管理基本是参考tcmalloc来进行的

golang内存管理的本质就是一个内存池,只不过对其多了很多优化,比如自动伸缩内存池大小,合理的切割内存块等等。

golang内存结构

首先,先了解golang的内存结构

  • 页 page:大小是8kb的内存空间
  • span:内存块,多个页组成的内存空间。如果把page比作一个工人的话,span就是一个工人小组,最少一个page
  • object:对象,就是内存分配的单位。span初始化时,会被切割成一堆等大的object,所谓的内存分配,就是分配object
  • sizeclass:空间规格,每个span都有一个object,记录了span该如何使用page

内存池 mheap

golang启动的时候,会一次性的从操作系统那里申请一大块内存作为内存池,这块内存空间会存在mheap结构中。mheap负责将这块内存切割成不同的大小的区域,并将其中一部分内存切割成合适的大小,分配给用户使用

mcentral

用途相同的span会用链表组织分类在一起,这里的用途用sizeclass表示。在分配内存时,会先根据内存匹配sizeclass,然后根据sizeclass找到可用的span,并从这个span中取一个object给上层使用。存放这些span的地方就是mcentral

mcache

mcentral上是有锁,因为可能存在竞争关系,多个协程同时从mcentral申请内存,需要加锁来避免冲突

而golang又有高并发的特性,协程可以开很多,加锁会耽误并发效率。所以就在mcentral前面加了一层mcahche,协程可以从chache中申请内存。

这个mcache其实是和逻辑处理器P(GMP中的p)对应的,而同一时间一个P只能和一个线程绑定,不可能出现竞态条件,就不需要加锁。

对象分类

go会根据不同对象的大小分配到不同的span中,按照对象的大小,分成了tiny对象、小对象、大对象。

tiny微小对象:小于16字节的对象,会直接分配到mache中

小对象:16-32字节的对象

大对象:32以上,会被分配到mheap中

总结

这种设计之所以会这么快,主要有以下几个优势:

  1. 内存分配大部分是在用户态完成的,不需要频繁进入内核态
  2. 每个P都有独立的span cache(mcache),多个cpu不会并发的读取同一块内存,可以增加cpu内存命中率
  3. 内存碎片的问题,是go自己在用户态管理的,减少了操作系统层面对碎片的管理压力
  4. 因为每个P都有独立的mcache,不要加锁

总体来说,go的内存管理是一个金字塔结构:

image.png

逃逸分析

逃逸分析就是分析内存是分配到栈上还是堆上,这个过程是在编译阶段执行的

栈上的空间会在函数结束调用之后自动回收,而堆上的空间才是需要go内存管理分配的空间,这个过程就是上述调用mheap分割span,mcahe分配空间的步骤

内存逃逸的几个条件

  • 变量在函数外被引用一定分配到堆上

    • 函数返回值是指针类型
  • 变量内存过大,被优先分配到堆上

    • 比如声明了一个容量特别大的切片
  • 变量占用内存空间不确定

    • 比如无法确定变量的数据类型(interface)
    • 切片的声明使用了变量作为容量,因为逃逸分析是在编译阶段分析,无法确定占用内存

GMP和GC

内存管理需要有申请空间和释放空间两个步骤

  • go的调度模型是GMP,go的每个逻辑处理器P都会绑定一个M,每次执行的时候都会在从p的本地队列中取一个G调度到M上执行。同样的,每个P也会绑定一个mcache,本队队列上的G申请空间的时候会从mcache中申请,这样就防止了多个协程并发访申请同一个地址的问题,避免了加锁的操作

  • 释放空间由GC完成,golang GC过程只针对堆空间,采用了三色标记法和混合写屏障的策略,这样在标记阶段避免了STW,减少了系统卡段。但是仍需要在写屏障开启和关闭的阶段开启STW