基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Go 语言的内存分配策略是怎样的?

知识点图片

Go 语言的内存分配策略是其高性能并发的关键之一。简单来说,Go 的内存分配器是基于 Google 的 TCMalloc (Thread-Caching Malloc) 算法改进而来的。

其核心思想可以概括为:多级缓存、无锁分配(针对小对象)、隔离大小

以下是 Go 内存分配策略的详细解析:


1. 核心设计原则

  1. 内存自主管理:Go 运行时(Runtime)向操作系统申请一大块内存(Arena),然后自己管理,减少系统调用(Syscall)的开销。
  2. 分级分配:根据对象的大小,使用不同的分配策略(微小对象、小对象、大对象)。
  3. 本地缓存(Thread Cache):为每个逻辑处理器(P)提供本地缓存,绝大多数分配不需要加锁,极大地提高了并发性能。

2. 内存分配的层级结构

Go 的内存分配器由三级组件组成,类似于 CPU 的 L1/L2/L3 缓存架构:

A. mcache (线程/处理器缓存)

  • 位置:绑定在 P (Processor) 上。
  • 特点无锁。因为每个 P 在同一时间只能运行一个 Goroutine,所以访问 mcache 不需要加锁,速度极快。
  • 作用:维护了不同规格(Size Class)的 mspan 列表,用于快速分配小对象。

B. mcentral (中心缓存)

  • 位置:全局共享。
  • 特点需要加锁
  • 作用:当 mcache 中的内存用完时,会向 mcentral 申请。mcentral 收集了各种规格的 mspan,负责在不同 P 之间平衡内存使用。

C. mheap (堆内存)

  • 位置:全局唯一。
  • 特点需要加锁(粒度较大)。
  • 作用:内存管理的源头。当 mcentral 也不够用时,会向 mheap 申请。mheap 负责向操作系统申请大块内存(Arena),并将其切割成 mspan 管理。

3. 内存管理单元:mspan

Go 将内存页(Page,通常为 8KB)组合成 mspan,这是内存管理的基本单元。

  • Size Class(规格):Go 将内存划分为约 67 种不同的规格(例如 8B, 16B, 32B ... 32KB)。
  • 每个 mspan 只能存储特定规格的对象。例如,一个规格为 16B 的 mspan 会被切分成许多个 16B 的小块。
  • 好处:这种固定规格的分配方式极大地减少了内存碎片

4. 分配流程(按对象大小分类)

Go 根据对象的大小(Size)采取三种不同的分配策略:

A. 微小对象 (Tiny Objects) < 16B

  • 策略合并分配
  • 过程:为了节省内存,Go 会将多个微小对象(如 bool, int8)拼凑在一个 16B 的内存块中。这类似于“拼车”。
  • 目的:减少碎片,提高缓存局部性。

B. 小对象 (Small Objects) 16B ~ 32KB

  • 策略多级缓存分配
  • 过程
    1. 计算规格:确定对象属于哪个 Size Class(例如申请 20B,会匹配到 32B 的规格)。
    2. mcache:去当前 P 的 mcache 中找对应规格的 mspan。如果有空闲块,直接分配(无锁,极快)。
    3. mcentral:如果 mcache 满了,向 mcentral 申请一个新的 mspan(需要加锁)。拿到后放入 mcache 继续分配。
    4. mheap:如果 mcentral 也空了,向 mheap 申请(需要加锁)。
    5. 问 OS:如果 mheap 也不够,向操作系统申请新的虚拟内存。

C. 大对象 (Large Objects) > 32KB

  • 策略直接分配
  • 过程:跳过 mcachemcentral,直接向 mheap 申请分配一组连续的页(mspan)。
  • 原因:大对象数量少,但占用空间大,不适合放在缓存中,否则容易频繁触发缓存淘汰。

5. 逃逸分析 (Escape Analysis)

在讨论堆分配之前,必须提到逃逸分析。并不是所有变量都会分配在堆上。

  • 栈 (Stack):函数内部的局部变量,生命周期随函数结束而结束的,通常分配在栈上。栈分配极其廉价(只是移动栈指针)。
  • 堆 (Heap):如果编译器发现变量在函数返回后仍被引用(例如返回了指针),或者变量太大(栈放不下),或者大小不确定,该变量就会“逃逸”到堆上。

Go 的策略是:能分配在栈上,绝不分配在堆上。 只有逃逸的变量才走上述的 mcache/mcentral/mheap 流程。


6. 总结图解

plaintext
Goroutine 申请内存
      |
      v
  对象大小?
  /      \       \
<16B    16B~32KB   >32KB
 |         |         |
Tiny分配   |         |
(组合包)   |         |
 |         v         v
 +-----> mcache (本地无锁)
           | 没货?
           v
         mcentral (共享,有锁)
           | 没货?
           v
         mheap (全局,有锁) <------- 大对象直接走这里
           | 没货?
           v
        OS (操作系统)

总结 Go 内存分配的优势

  1. 速度快:通过 mcache 实现小对象的无锁分配,覆盖了绝大多数场景。
  2. 碎片少:通过 mspan 的固定规格(Size Class)管理,避免了外部碎片。
  3. 利用率高:Tiny 对象合并分配,节省空间。
  4. GC 友好:内存块的布局和元数据设计(Bitmap)有助于垃圾回收器快速扫描。
00:00
00:00