Go 的内存管理之管理模型

105 阅读5分钟

管理模型

  • 程序动态申请内存空间, 是要使用系统调用的, 比如 Linux 系统上是调用 mmap 方法实现的. 但对于大型系统服务来说, 直接调用 mmap 申请内存,会有一定的代价. 比如:

    • 系统调用会导致进程进入内核态, 内核分配完内存后(对虚拟地址和物理地址进行映射等操作), 再返回用户态.
    • 频繁申请很小内存空间, 容易出现大量内存碎片, 增大OS整理碎片的压力.
    • 为了保证内存访问具有良好的局部性, 开发者需要投入精力去做优化, 这是一个很重要的负担.

    如何解决上面的问题呢? 有经验的人, 可能很快就想到了解决方案, 那就是我们常说的 对象池 (也可以说是缓存)

    假设系统需要频繁动态申请内存来存放一个数据结构, 比如 [10]int. 那么我们完全可以在程序启动之初, 一次性申请几百甚至上千个 [10]int. 这样就完美的解决了上面遇到的问题:

    • 不需要频繁申请内存了, 而是从对象池里拿, 程序不会频繁进入内核态
    • 因为一次性申请一个连续的大空间, 对象池会被重复利用, 不会出现碎片
    • 程序频繁访问的就是对象池背后的同一块内存空间, 局部性良好

    这样会造成一定的内存浪费, 我们可以定时检测对象池的大小, 保证可用对象的数量在一个合理的范围, 少了就提前申请, 多了就自动释放.

    如果某种资源的申请和回收是昂贵的, 我们都可以通过建立资源池的方式来解决, 比如连接池, 内存池等等, 都是一个思路.

  • Go的内存管理本质

    就是一个内存池, 只不过内部做了很多的优化. 比如自动伸缩内存池大小, 合理的切割内存块等等.

  • 概念

    • page: 内存页, 一块 8K 大小的内存空间. Go 与 OS之间的内存申请和释放都是以page 为单位的

    • span: 内存块, 一个或多个连续的 page 组成一个span. 如果把 page 比喻成工人, span可以看成是小队, 工人被分成若干个队伍, 不同队伍干不同的(sizeclass)活

    • sizeclass: 空间规格, 每个 span 都带有一个 sizeclass , 标记着该 span 中的 page 应该如何使用. 标志着 span 是一个什么样的队伍.

    • object: 对象, 用来存储一个变量数据内存空间, 一个 span初始化时,会被切割成一堆等大的object. 假设 object 的大小是 16B, span 大小是 8K, 那么就会把span中的 page 就会被初始化 8K / 16B = 512object . 所谓内存分配, 就是分配一个 object 出去.

    • 内存碎片

      系统(OS/各种runtime)在内存管理过程中, 会不可避免的出现一块块无法被使用的内存空间, 这就是内存管理的产物.

    • 内部碎片

      一般都是因为字节对齐,如上面介绍 Tiny 对象分配的部分; 为了字节对齐, 会导致一部分空间直接被放弃掉, 不做分配使用.

    • 外部碎片

      一般时因为内存的不断分配和释放, 导致一些释放的小内存块分散在内存各处, 无法被用以分配. 不过Go的内存管理机制不会引起大量外部碎片.

    不同颜色代表不同的 span

    不同spansizeclass 不同, 表示里面的 page 将会按照不同的规格切割成一个个等大的 object 用作分配 span:用来存储 page 和 span 信息,比如一个 span 的起始地址是多少,有几个page, 已经使用了多大等等。

​ bitmap:存储着各个 span 中对象的标记信息,比如对象是否可回收等等。

​ arena_start:将要分配给应用程序使用的空间

  • 内存池 mheap

    Go 的程序在启动之初, 会一次性从OS那里申请一大块内存作为内存池. 这块内存空间会放在一个叫 mheapstruct 中管理, mheap 负责将这一整块内存切割成不同的区域, 并将其中一部分的内存切割成合适的大小, 分配给用户使用.

  • mcentral

    用途相同(sizecliass 相同, 用来存储哪种大小的对象)的 span 会以链表的形式组织在一起. 比如当分配一块大小为 n 的内存时, 系统计算 n 应该使用哪种 sizeclass , 然后根据 sizeclass的值去找到一个可用的 span 来用作分配.

    找到合适的 span 后, 会从中取一个 object 返回给上层使用. 这些 span 被放在一个叫做 mcentral 的结构中管理.

    mheap 将从 OS 那里申请过来的内存初始化成一个大 span(sizeclass=0). 然后根据需要从这个大 span 中切出小 span , 放在mcentral中来管理.

    spanmheap.freelargemheap.busylarge 等管理.

    如果 mcentral 中的 span 不够用了, 会从 mheap.freelarge 上再切一块, 如果 mheap.freelarge 空间不够, 会再次从OS那里申请内存重复上述步骤. 这种方式可以避免出现外部碎片, 因为同一个 span 是按照固定大小分配和回收的, 不会出现不可利用的一小块内存把内存分割掉.