菜单

内存管理

相关源文件

本文档描述了 Go 运行时中的内存管理系统。它涵盖了 Go 程序中内存的分配、组织和回收方式,重点关注堆分配系统、内存结构和分配机制。有关垃圾回收的具体信息,请参阅 垃圾回收

Go 内存管理概述

Go 的内存管理旨在实现快速、并发,并最大限度地减少碎片。该系统结合使用了线程局部缓存、中央空闲列表和堆分配器,可在无需程序员手动干预的情况下高效地管理内存。

Go 内存管理的关键特性

  • 自动内存管理:内存分配和释放无需程序员直接控制。
  • 并发:旨在高效地在多线程环境中工作。
  • 低延迟:在常见情况下,分配已优化为非常快速。
  • 大小分段:内存根据分配大小以不同方式进行管理。
  • 非压缩:对象分配后不会移动(简化了与 C 的互操作性)。

来源:src/runtime/malloc.go5-76 src/runtime/mgc.go1-120

内存架构

Go 的内存管理系统采用分层结构,在性能和内存效率之间取得平衡。

内存分配层级结构

主要组件有:

  • mheap:主分配器,以页面粒度(8KB 页面)管理内存。它是所有内存的来源,并直接管理大块分配。

  • mspan:一个页面块,包含特定大小类对象的内存。每个 mspan 管理相同大小对象的内存。

  • mcentral:收集给定大小类的所有 span。当 mcache 需要更多特定大小类的内存时,它会从相应的 mcentral 获取。

  • mcache:每个 P(处理器)的 mspan 免费空间缓存。这使得大多数分配无需锁即可进行。

来源:src/runtime/malloc.go10-76 src/runtime/mheap.go56-180 src/runtime/mcache.go1-50 src/runtime/mcentral.go1-100

虚拟内存布局

堆包括

  • Arena:固定大小的虚拟内存区域(64 位系统上为 64MB,32 位系统上为 4MB),与自身大小对齐。

  • Arena Map:一个两级索引,从虚拟地址映射到 arena 元数据,覆盖整个可能的地址空间。

  • heapArena:每个 arena 的元数据,包含

    • Heap Bitmap:标记内存中的哪些字包含指针(对垃圾回收很重要)。
    • Span Map:标识 arena 中每个页面由哪个 span 管理。
  • Spans:连续页面的运行,管理相同大小类的对象。

  • Objects:程序实际使用的分配内存。

来源:src/runtime/malloc.go77-254 src/runtime/mheap.go62-176 src/runtime/mbitmap.go5-54

内存分配流程

大小分类

Go 根据大小将分配分为不同的类别。

  • Tiny:≤16 字节,不含指针。这些可以打包在一起以减少碎片。
  • Small:≤32KB,向上取整到约 70 个大小类别之一。
  • Large:>32KB,直接从堆分配。

分配流程

分配过程遵循以下步骤:

  1. 确定分配类型,根据大小和指针内容。
  2. 对于小分配::
    • 向上取整到最近的大小类别。
    • 在相应的 mcache span 中查找。
    • 如果 span 已满,则从 mcentral 获取新 span。
    • 如果 mcentral 为空,则从 mheap 获取新 span。
  3. 对于大分配::
    • 直接从 mheap 分配。
  4. 内存清零:
    • 内存要么在分配时清零初始化,要么在获取 span 时清零。

来源:src/runtime/malloc.go10-76 src/runtime/malloc.go650-800

关键分配函数

Go 中主要的分配路径涉及以下关键函数:

  1. mallocgc:内存分配的主要入口点。
  2. mcache.nextFree:从 P 的缓存中快速分配。
  3. mcache.refill:当 mcache 为空时,从 mcentral 获取新 span。
  4. mcentral.cacheSpan:为 mcache 提供 span。
  5. mheap.alloc:从堆中分配 span。

来源:src/runtime/malloc.go800-1000

内存回收

Span 回收

当对象被垃圾回收器释放时,内存并不会立即返回给操作系统。而是:

  1. 包含空闲对象的 Span 会保留在 mcentral 的空闲列表中。
  2. 完全空闲的 Span 会被返回到 mheap。
  3. mheap 可能会通过整理(scavenging)将内存返回给操作系统。

来源:src/runtime/mgcsweep.go1-58 src/runtime/mgcscavenge.go1-32

内存整理

Go 使用整理机制将未使用的内存返还给操作系统。

  • 后台整理程序:一个单独的 goroutine,会定期扫描并释放未使用的内存。
  • 分配时整理:当分配新 span 时,如果满足某些阈值,则会进行内存整理。

整理程序会尝试在内存保留(以备将来使用)和 RSS(驻留集大小)减少之间取得平衡,使用基于近期分配模式的启发式方法。

来源:src/runtime/mgcscavenge.go5-54

内存统计与限制

Go 维护详细的内存统计数据,以指导分配、垃圾回收和整理决策。

  • MemStats:可供程序访问的运行时内存统计信息。
  • Heap Goal:基于 GOGC 设置计算的目标堆大小。
  • GC Trigger:触发垃圾回收的时间点。
  • Memory Limit:可选的堆大小硬限制(通过 GOMEMLIMIT)。

这些信息决定了何时发生垃圾回收以及内存整理的积极程度。

来源:src/runtime/mgcpacer.go1-60

栈内存管理

尽管本文档主要关注堆内存,但值得注意的是 Go 中的栈是:

  • 动态大小(根据需要增长和缩小)。
  • 从堆中分配,但单独管理。
  • 不进行垃圾回收(在 goroutine 退出时被释放)。

栈增长是通过检测栈溢出并在需要时分配更大的栈来处理的。

来源:src/runtime/stack.go1-80

MCache和页面分配

MCaches

每个 P(处理器)都有一个本地 mcache,其中包含每个大小类的空闲对象的 span。这使得大多数分配可以无需锁定即可进行,在常见情况下提供非常快速的分配。

mcache 代表内存分配层级结构的第一级,旨在最大限度地减少多线程程序中的争用。

当 mcache 中的 span 填满时,会从 mcentral 获取新的 span。

来源:src/runtime/mcache.go1-100

页面分配

在最底层,Go 内存系统以页面(通常为 8KB)为单位管理内存。

  • 页面是内存管理的基本单位。
  • 它们以块的形式分配以形成 span。
  • 页面分配器会跟踪哪些页面正在使用。
  • Arena 等结构的大小是页面的倍数。

页面分配器使用位图来有效地跟踪堆中空闲和已使用的页面。

来源:src/runtime/mpagealloc.go1-25 src/runtime/mheap.go20-55

特殊内存分配情况

Tiny 分配

Go 为 tiny 分配(≤16 字节,不含指针)进行了优化。

  • 多个 tiny 分配会被打包到一个内存块中。
  • 这极大地减少了小字符串、小数字等数据的内存开销。
  • 有助于避免因大量小对象导致的堆碎片。

来源:src/runtime/malloc.go125-135

大对象

大于 32KB 的对象有不同的处理方式:

  • 直接从 mheap 分配。
  • 不受大小类别向上取整的影响。
  • 可能会跨越多个堆页面。

来源:src/runtime/malloc.go63-64

与垃圾回收的关系

内存管理与垃圾回收器紧密集成。

  1. 堆位图跟踪哪些字包含指针(用于 GC 遍历)。
  2. GC 决定何时对象不再被使用。
  3. 清扫(Sweeping)回收死亡对象的内存。
  4. GC 调度会影响内存的分配和释放时间。

有关垃圾回收工作原理的详细信息,请参阅 垃圾回收

来源: src/runtime/mgc.go1-120 src/runtime/mbitmap.go5-20

运行时内存管理 API

Go 的内存管理在很大程度上是自动化的,但运行时提供了一些供高级使用的 API

  • runtime.MemStats:提供详细的内存统计信息
  • runtime.GC():强制执行垃圾回收
  • runtime.SetFinalizer():为对象设置一个终结器函数
  • runtime.ReadMemStats():将 MemStats 结构体填充为当前的内存统计信息

这些 API 应谨慎使用,因为运行时通常会在无需干预的情况下优化内存管理。

来源: src/runtime/mstats.go src/runtime/mgc.go462-505