Golang 内存模型与分配机制(上) | 青训营

2023年 8月 28日 61.7k 0

Golang 内存模型与分配机制(上) | 青训营

2023/8/27 ·雨辰login

这篇介绍Go语言的内存管理机制

本篇内容引用自知乎用户@小徐先生.

这真的是一个宝藏博主,B站也有号:小徐先生1212

所以把他的笔记找来跟大家分享,希望大家都去看看,真的做的非常好。

0 前言

未来两周,想和大家探讨的主题是 Golang 内存管理机制.

本系列会分为两篇,第一篇谈及 Golang 内存模型以及内存分配机制,第二篇会和大家讨论 Golang 的垃圾回收机制. 本文是其中第一篇.

我个人比较推崇”基于源码支撑原理“的信念,所以本文在阐述原理的基础上,会伴有大量源码走读的过程,作为理论的支撑论证. 走读的 Go 源码版本为 1.19.

内存管理与垃圾回收都属 Go 语言最复杂的模块,受限于笔者个人水平,文章内容可能有不足或纰漏之处,很欢迎大家进行批评指正.

1 内存模型

1.1 操作系统存储模型

Golang 内存模型与分配机制(上) | 青训营-1

本文既然要聊到 Golang 的内存模型设计,就让我们首先回顾操作系统中经典的多级存储模型设计.

观察上图,我们可以从中捕捉到的关键词是:

  • 多级模型
  • 动态切换

1.2 虚拟内存与物理内存

操作系统内存管理中,另一个重要概念是虚拟内存,其作用如下:

  • 在用户与硬件间添加中间代理层(没有什么是加一个中间层解决不了的)
  • 优化用户体验(进程感知到获得的内存空间是“连续”的)
  • “放大”可用内存(虚拟内存可以由物理内存+磁盘补足,并根据冷热动态置换,用户无感知)

1.3 分页管理

操作系统中通常会将虚拟内存和物理内存切割成固定的尺寸,于虚拟内存而言叫作“页”,于物理内存而言叫作“帧”,原因及要点如下:

  • 提高内存空间利用(以页为粒度后,消灭了不稳定的外部碎片,取而代之的是相对可控的内部碎片)
  • 提高内外存交换效率(更细的粒度带来了更高的灵活度)
  • 与虚拟内存机制呼应,便于建立虚拟地址->物理地址的映射关系(聚合映射关系的数据结构,称为页表)
  • linux 页/帧的大小固定,为 4KB(这实际是由实践推动的经验值,太粗会增加碎片率,太细会增加分配频率影响效率)

1.4 Golang 内存模型

前几小节的铺垫,旨在从“内存模型设计”这件事情中收获一些触类旁通的设计理念.

下面步入正题,聊聊 Golang 的内存模型设计的几个核心要点:

  • 以空间换时间,一次缓存,多次复用

由于每次向操作系统申请内存的操作很重,那么不妨一次多申请一些,以备后用.

Golang 中的堆 mheap 正是基于该思想,产生的数据结构. 我们可以从两个视角来解决 Golang 运行时的堆:

I 对操作系统而言,这是用户进程中缓存的内存

II 对于 Go 进程内部,堆是所有对象的内存起源

  • 多级缓存,实现无/细锁化

堆是 Go 运行时中最大的临界共享资源,这意味着每次存取都要加锁,在性能层面是一件很可怕的事情.

在解决这个问题,Golang 在堆 mheap 之上,依次细化粒度,建立了 mcentral、mcache 的模型,下面对三者作个梳理:

  • mheap:全局的内存起源,访问要加全局锁
  • mcentral:每种对象大小规格(全局共划分为 68 种)对应的缓存,锁的粒度也仅限于同一种规格以内
  • mcache:每个 P(正是 GMP 中的 P)持有一份的内存缓存,访问时无锁

这些概念,我们在第 2 节中都会再作详细展开,此处可以先不深究,注重于宏观架构即可.

  • 多级规格,提高利用率

首先理下 page 和 mspan 两个概念:

(1)page:最小的存储单元.

Golang 借鉴操作系统分页管理的思想,每个最小的存储单元也称之为页 page,但大小为 8 KB

(2)mspan:最小的管理单元.

mspan 大小为 page 的整数倍,且从 8B 到 80 KB 被划分为 67 种不同的规格,分配对象时,会根据大小映射到不同规格的 mspan,从中获取空间.

于是,我们回头小节多规格 mspan 下产生的特点:

I 根据规格大小,产生了等级的制度

II 消除了外部碎片,但不可避免会有内部碎片

III 宏观上能提高整体空间利用率

IV 正是因为有了规格等级的概念,才支持 mcentral 实现细锁化

  • 全局总览,留个印象

上图是 Thread-Caching Malloc 的整体架构图,Golang 正是借鉴了该内存模型. 我们先看眼架构,有个整体概念,后续小节中,我们会不断对细节进行补充.

2 核心概念梳理

2.1 内存单元 mspan

分点阐述 mspan 的特质:

  • mspan 是 Golang 内存管理的最小单元
  • mspan 大小是 page 的整数倍(Go 中的 page 大小为 8KB),且内部的页是连续的(至少在虚拟内存的视角中是这样)
  • 每个 mspan 根据空间大小以及面向分配对象的大小,会被划分为不同的等级(2.2小节展开)
  • 同等级的 mspan 会从属同一个 mcentral,最终会被组织成链表,因此带有前后指针(prev、next)
  • 由于同等级的 mspan 内聚于同一个 mcentral,所以会基于同一把互斥锁管理
  • mspan 会基于 bitMap 辅助快速找到空闲内存块(块大小为对应等级下的 object 大小),此时需要使用到 Ctz64 算法.

mspan 类的源码位于 runtime/mheap.go 文件中:

type mspan struct {
    // 标识前后节点的指针 
    next *mspan     
    prev *mspan    
    // ...
    // 起始地址
    startAddr uintptr 
    // 包含几页,页是连续的
    npages    uintptr 


    // 标识此前的位置都已被占用 
    freeindex uintptr
    // 最多可以存放多少个 object
    nelems uintptr // number of object in the span.


    // bitmap 每个 bit 对应一个 object 块,标识该块是否已被占用
    allocCache uint64
    // ...
    // 标识 mspan 等级,包含 class 和 noscan 两部分信息
    spanclass             spanClass    
    // ...
}

2.2 内存单元等级 spanClass

mspan 根据空间大小和面向分配对象的大小,被划分为 67 种等级(1-67,实际上还有一种隐藏的 0 级,用于处理更大的对象,上不封顶)

下表展示了部分的 mspan 等级列表,数据取自 runtime/sizeclasses.go 文件中:

class bytes/obj bytes/span objects tail waste max waste
1 8 8192 1024 0 87.50%
2 16 8192 512 0 43.75%
3 24 8192 341 8 29.24%
4 32 8192 256 0 21.88%
...
66 28672 57344 2 0 4.91%
67 32768 32768 1 0 12.50%

对上表各列进行解释:

(1)class:mspan 等级标识,1-67

(2)bytes/obj:该大小规格的对象会从这一 mspan 中获取空间. 创建对象过程中,大小会向上取整为 8B 的整数倍,因此该表可以直接实现 object 到 mspan 等级 的映射

(3)bytes/span:该等级的 mspan 的总空间大小

(4)object:该等级的 mspan 最多可以 new 多少个对象,结果等于 (3)/(2)

(5)tail waste:(3)/(2)可能除不尽,于是该项值为(3)%(2)

(6)max waste:通过下面示例解释:

以 class 3 的 mspan 为例,class 分配的 object 大小统一为 24B,由于 object 大小

相关文章

服务器端口转发,带你了解服务器端口转发
服务器开放端口,服务器开放端口的步骤
产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
如何使用 WinGet 下载 Microsoft Store 应用
百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

发布评论