Go内存分配与结构(两张图就懂了)

505 阅读5分钟

这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战

内存空间主要包括两个部分:栈、堆

发生函数调用进行参数传递、返回值传递、局部变量大多分配在栈上,由编译器管理

存储工程师申请的对象等,一般放在堆上,由用户、编译器共同管理

Go内存管理过程

三个组件:用户程序(Mutator)、分配器(Allocator)、收集器(Collector)

分配器

  • 内存分配器:用户程序申请内存时,负责申请新的内存(从堆中初始化内存)

分配方法

  • 线性分配器
    • 只有一个指针
      • 指向剩余的大量空余空间
      • 分配空间时,指针往后移动
    • 单独使用线性分配器没有办法重用已被回收的内存
      • 需要与垃圾回收算法配合使用
        • 标记压缩
        • 复制内存
        • 分代回收
      • 可以整理内存碎片,调高内存利用率
  • 空闲链表分配器
    • 通过链表的形式将空闲内存连接起来
      • 申请内存遍历所有的内存块,O(n)
      • 如何选择使用的内存块
        • 首次适应:第一个满足的大小要求的内存块
        • 循环首次遍历:从上一次遍历的地方开始首次适应
        • 最优适应:遍历整个链表,选择最优解
        • 隔离适应:分割成多个链表,每个链表的内存块大小不一样,但是同链表上内存块大小一致,申请时先选择链表,在选择链表上的内存块
      • Go使用的是类似第四种策略
        • 分级分配
          • 线程缓存分配:据说币malloc还要快
          • Go借鉴了线程缓存分配
          • 多级缓存,根据大小分类,根据类别实施
          • 大小分类:大小会影响分配内存过程与开销,分开处理可以提高性能
          • 多级缓存:引入了线程缓存、中心缓存、页堆
          • 线程缓存:分配小对象内存,属于每个独立线程,不存在锁等,能够提高部分性能
          • 中心缓存:分配小对象内存,线程缓存不满足需求的时候(如多线程),会使用中心缓存
          • 页堆:分配超过32KB以上对象,分配大内存

虚拟内存布局

  • Go1.11以前使用的是线性内存,这里不做过多讨论
  • Go1.11后使用稀疏内存
    • 解决堆大小上限问题
    • 解决C、Go混合使用内存带来的冲突问题
    • 内存失去连续性,管理更加复杂
  • 二维稀疏内存
    • 通过二维runtime.heapArena数组管理内存
      • heapArena中有指针指向对应管理的内存位置
    • 每一个管理64MB
    • 不同平台上的二维数组大小不一样
  • 地址空间
    • 所有的内存都是由实际操作系统中申请获得的,由实际内存地址空间映射到Go的管理空间,也就是自己做了一层抽象
    • 有四种不同的状态来表示对应地址的状态
    • 同时go也实现了操作系统内存操作的方法

内存管理组件

  • Go在启动的时候会为每个处理器都分配一个线程缓存
  • 线程缓存(mcache),mcache存会管理特定大小的对象,持有mspan(内存管理单元)
  • mspan没有空闲对象后从mheap持有的134个中心缓存mcentral中获取新的内存单元
  • 中心缓存属于全局的堆结构体mheap
  • mheap会从操作系统中申请内存

接下来从小到大进行介绍

内存管理单元

mspan(内存管理单元)是最基本的内存管理单元

mspan之间会构建成双向链表

type mspan struct {
	next *mspan
	prev *mspan
	
  startAddr uintptr // 起始地址
	npages    uintptr // 页数,每页大小为8KB,为操作系统内存页的整数倍
	freeindex uintptr // 扫描页中空闲对象的初始索引

	allocBits  *gcBits // 内存占用情况
	gcmarkBits *gcBits // 内存回收情况
	allocCache uint64  // allocBits补码,快速查找未使用内存

	state       mSpanStateBox // 描述当前mspan的状态,状态转换为原子性的,避免垃圾回收的线程竞争问题
	state       mSpanStateBox // 跨度类,决定内存存储单元中存储对象的大小、个数
}
  • 内存不足的时候,以页为单位向堆申请内存
  • 用户程序或线程向mspan申请内存的时候,可以通过allocCache快速查看未分配的内存
    • 无空闲内存的时候,调用refill更新内存管理单元
  • Go中有67中跨度类,每种跨度类的对象大小、页数等都不一样

线程缓存

即mcache,与线程(M)绑定,在alloc字段中有68*2个mspan

初始化

  • 会从mheap中获取mspan结构体,获取的mspan结构体中都是空的占位符

微分配器

专门处理16字节以下的对象分配(非指针类型)

中心缓存

即mcentral,不独属于某个线程,因此访问需要上锁(悲观锁)

  • 每个中心缓存只管理某个跨度类
  • 持有两个spanSet,分别管理空闲以及忙碌的mspan
  • 线程缓存也是通过中心缓存获取mspan的
  • 扩容,从mheap中获取对应跨度类的新mspan

页堆

即mheap

  • 为全局变量
  • 统一管理堆上分配的对象
  • 包含一个mcentral数组
  • 包含heapArena字段,进行二维矩阵内存的管理
    • 每个管理64M空间

内存分配

微对象

微型分配器(位于线程缓存上)-》线程缓存-》中心缓存-》堆

  • 小于16字节

微分配器:

  • 主要用于分配较小字符串、逃逸的临时变量
  • 对象不可是指针类型
  • 一个内存块管理多个微对象
    • 所有对象可回收时,整个内存才可被回收

小对象

线程缓存-》中心缓存-》堆

  • 小于16字节的指针对象
  • 16~32768字节的对象

大对象

  • 会去计算需要多少npages,然后返回一个只能容纳一个该跨度类的mspan,管理对应数量的npages