go的内存管理

基础

内存结构

内存结构

arena: go从堆中分配的内存,都放到这里,基本单位是一页8K
bitmap: 表示arena区哪些地址保存了对象,哪些地址保存了指针。一个byte(8 bit)对应了arena区域中的四个指针大小的内存
spans: 存的是span指针,用于表示arena区域中的下标对应的页属于哪个span

span

管理实际用来分配的内存块(span)的管理空间,一个span的大小,最小8K(一页)

  • 按照大小分类:span会根据大小划分不同的类型,以减少外部内存碎片的产生,一共67种
  • 按照是否是包含指针:scan类型(包含指针),noscan类型(不包含指针)

mspan

包含多个类型相同的span的一个双向链表

从heap中分配对象

可以通过逃逸分析,来分析

  • 返回对象的指针
  • 传递了对象的指针到其他函数
  • 在闭包中使用了对象并且需要修改对象
  • 使用new

go内存分配

关于内存分配,需要满足的需求

  • 减少内存碎片
  • 提高内存分配效率

减少内存碎片

内存碎片

内部碎片:由于采用固定大小的内存分区,当一个进程不能完全使用分给它的固定内存区域时就产生了内部碎片,通常内部碎片难以完全避免
外部碎片:外部碎片是由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。

减少碎片设计

如果内存分配按照需求随意从内存中创建对象和消耗,最后肯定会产生大量的碎片。如图所示


image.png

减少碎片化的方法

内存之所以产生碎片,是因为不断的按照随机大小分配,最后倒置,大量的连续区域过小无法分配内存。因此,按照对象的尺寸划分好一个个的内存单元,按照实际大小放到对应的内存单元里面去。会处是必然造成,内部碎片的产生。

go中span的类型

具体的可以看看这里: 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         32        8192      256           0     46.88%
//     4         48        8192      170          32     31.52%
//     5         64        8192      128           0     23.44%
.....

class:span的分级id
bytes/obj:这个class的span里可以存放的对象的大小的上限。即,这个class的span可以存放的对象为:上一级span大小上限 + 1 <= 对象大小 <= 当前span大小上限
bytes/span:最低占用一个Page,即8 KB(8192 bytes),上涨都是按Page倍数来
objects:这个class的span一个可存放的对象数量上限。
tail waste:在span中对象满载的情况下,因对象数量无法被整除而浪费的内存
max waste:最大内存浪费情况,每一个放进该span的对象大小都是最小值的情况,浪费的内存量

提高内存分配效率

采取了多层级分配策略,形成无锁化或者降低锁的粒度,来提高内存分配效率。


image.png

分层

mcache: P中的span管理,一总共有 67 * 2 = 134种 mspan。
性能提升:

  • 由于一个P只有一个线程在上面跑,所以不需要加锁。
  • 使用数组,通过下标来做映射,快速找到需要的span类型
 type mcache struct { alloc [numSpanClasses]*mspan .... }

mcentral: 是mheap的一部分,用于缓存,保证两种类型的mspanList,

  • empty mspanList: 没有空的span或者是已经被mcache缓存
  • nonempty mspanList:有空闲对象的span列表

mheap: 顶层的分配器,分为两部分,一部分是还没有划分到mcentral的span,一部分是划分到mcentral的。

性能提升:

  • 当mcache没有可用的内存块时,通过span的类型找到对应的mcentral,仅仅在对应类的mcentral上加锁就好,减少了锁的粒度。

  • 使用数组,通过下标来做映射

numSpanClasses = 134
// 当mcache没有可用的内存块时,通过需要的span的类型找到对应的mcentral,仅仅在对应类的mcentral上加锁就好,减少了锁的粒度
central [numSpanClasses]struct {
    mcentral mcentral
    pad      [sys.CacheLineSize unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}

分配流程

  • 大于 32K 的大对象直接从 mheap 分配。
  • 小于 16B 的使用 mcache 的微型分配器分配
  • 对象大小在 16B ~ 32K 之间的的,首先通过计算使用的大小规格,然后使用 mcache 中对应大小规格的块分配
  • 如果对应的大小规格在 mcache 中没有可用的块,则向 mcentral 申请
  • 如果 mcentral 中没有可用的块,则向 mheap 申请,并根据 BestFit 算法找到最合适的 mspan。如果申请到的 mspan 超出申请大小,将会根据需求进行切分,以返回用户所需的页数。剩余的页构成一个新的 mspan 放回 mheap 的空闲列表。
  • 如果 mheap 中没有可用 span,则向操作系统申请一系列新的页

关于内存使用统计

关于内存的统计信息,可以从下面的结构体当中获取,代码位置
golang相关的内存使用的监控信息,就是从该结构体获取的,可以知道从操作系统中获取了多少内存,使用了多少堆内存,有多少堆内存没有被使用等等信息

// A MemStats records statistics about the memory allocator.
type MemStats struct {
    // 常规统计。

    // Alloc 是已分配的堆内存对象占用的内存量(bytes)。
    //
    // 这个值和 HeapAlloc 一致(看下面)。
    Alloc uint64

    // TotalAlloc 是累积的堆内存对象分配的内存量(bytes)。
    //
    // TotalAlloc 会随着堆内存对象分配慢慢增长,但不像 Alloc 和 HeapAlloc,
    // 这个值不会随着对象被释放而缩小。
    TotalAlloc uint64

    // Sys 是从 OS 获得的内存总量(bytes)。
    //
    // Sys 是下面列出的 XSys 字段的综合。Sys 维护着为 Go 运行时预留的虚拟内存空间地址,
    // 里面包含了:堆、栈,以及其他内部数据结构。
    Sys uint64

    // Lookups 是 runtime 执行的指针查询的数量。
    //
    // 这主要在针对 runtime 内部进行 debugging 的时候比较有用。
    Lookups uint64

    // Mallocs 是累积被分配的堆内存对象数量。
    // 存活堆内存对象数量是 Mallocs - Frees。
    Mallocs uint64

    // Frees 是累积被释放掉的堆内存对象数量。
    Frees uint64

    // 堆内存统计。
    //
    // 理解堆内存统计需要一些 Go 是如何管理内存的知识。Go 将堆内存虚拟内存空间以 "spans" 为单位进行分割。
    // spans 是 8K(或更大)的连续内存空间。一个 span 可能会在以下三种状态之一:
    //
    // 一个 "空闲 idle" 的 span 内部不含任何对象或其他数据。
    // 占用物理内存空间的空闲状态 span 可以被释放回 OS(但虚拟内存空间不会),
    // 或者也可以被转化成为 "使用中 in use" 或 "堆栈 stack" 状态。
    //
    // 一个 "使用中 in use" span 包含了至少一个堆内存对象且可能还有富余的空间可以分配更多的堆内存对象。
    //
    // 一个 "堆栈 stack" span 是被用作 goroutine stack 的 内存空间。
    // 堆栈状态的 span 不被视作是堆内存的一部分。一个 span 可以在堆内存和栈内存之间切换;
    // 但不可能同时作为两者。

    // HeapAlloc 是已分配的堆内存对象占用的内存量(bytes)。
    //
    // "已分配"的堆内存对象包含了所有可达的对象,以及所有垃圾回收器已知但仍未回收的不可达对象。
    // 确切的说,HeapAlloc 随着堆内存对象分配而增长,并随着内存清理、不可达对象的释放而缩小。
    // 清理会随着 GC 循环渐进发生,所有增长和缩小这两个情况是同时存在的,
    // 作为结果 HeapAlloc 的变动趋势是平滑的(与传统的 stop-the-world 型垃圾回收器的锯齿状趋势成对比)。
    HeapAlloc uint64

    // HeapSys 是堆内存从 OS 获得的内存总量(bytes)。
    //
    // HeapSys 维护着为堆内存而保留的虚拟内存空间。这包括被保留但尚未使用的虚拟内存空间,
    // 这部分是不占用实际物理内存的,但趋向于缩小,
    // 和那些占用物理内存但后续因不再使用而释放回 OS 的虚拟内存空间一样。(查看 HeapReleased 作为校对)
    //
    // HeapSys 用来评估堆内存曾经到过的最大尺寸。
    HeapSys uint64

    // HeapIdle 是处于"空闲状态(未使用)"的 spans 占用的内存总量(bytes)。
    //
    // 空闲状态的 spans 内部不含对象。这些 spans 可以(并可能已经被)释放回 OS,
    // 或者它们可以在堆内存分配中重新被利用起来,或者也可以被重新作为栈内存利用起来。
    //
    // HeapIdle 减去 HeapReleased 用来评估可以被释放回 OS 的内存总量,
    // 但因为这些内存已经被 runtime 占用了(已经从 OS 申请下来了)所以堆内存可以重新使用这些内存,
    // 就不用再向 OS 申请更多内存了。如果这个差值显著大于堆内存尺寸,这意味着近期堆内存存活对象数量存在一个短时峰值。
    HeapIdle uint64

    // HeapInuse 是处于"使用中"状态的 spans 占用的内存总量(bytes)。
    //
    // 使用中的 spans 内部存在至少一个对象。这些 spans 仅可以被用来存储其他尺寸接近的对象。
    //
    // HeapInuse 减去 HeapAlloc 用来评估被用来存储特定尺寸对象的内存空间的总量,
    // 但目前并没有被使用。这是内存碎片的上界,但通常来说这些内存会被高效重用。
    HeapInuse uint64

    // HeapReleased 是被释放回 OS 的物理内存总量(bytes)。
    //
    // 这个值计算为已经被释放回 OS 的空闲状态的 spans 堆内存空间,且尚未重新被堆内存分配。
    HeapReleased uint64

    // HeapObjects 是堆内存中的对象总量。
    //
    // 和 HeapAlloc 一样,这个值随着对象分配而上涨,随着堆内存清理不可达对象而缩小。
    HeapObjects uint64

    // 栈内存统计。
    //
    // 栈内存不被认为是堆内存的一部分,但 runtime 会将一个堆内存中的 span 用作为栈内存,反之亦然。

    // StackInuse 是栈内存使用的 spans 占用的内存总量(bytes)。
    //
    // 使用中状态的栈内存 spans 其中至少有一个栈内存。这些 spans 只能被用来存储其他尺寸接近的栈内存。
    //
    // 并不存在 StackIdle,因为未使用的栈内存 spans 会被释放回堆内存(因此被计入 HeapIdle)。
    StackInuse uint64

    // StackSys 是栈内存从 OS 获得的内存总量(bytes)。
    //
    // StackSys 是 StackInuse 加上一些为了 OS 线程栈而直接从 OS 获取的内存(应该很小)。
    StackSys uint64

    // 堆外(off-heap)内存统计。
    //
    // 下列的统计信息描述了并不会从堆内存进行分配的运行时内部(runtime-internal)结构体(通常因为它们是堆内存实现的一部分)。
    // 不像堆内存或栈内存,任何这些结构体的内存分配仅只是为这些结构服务。
    //
    // 这些统计信息对 debugging runtime 内存额外开销非常有用。

    // MSpanInuse 是 mspan 结构体分配的内存量(bytes)。
    MSpanInuse uint64

    // MSpanSys 是为 mspan 结构体从 OS 申请过来的内存量(bytes)。
    MSpanSys uint64

    // MCacheInuse 是 mcache 结构体分配的内存量(bytes)。
    MCacheInuse uint64

    // MCacheSys 是为 mcache 结构体从 OS 申请过来的内存量(bytes)。
    MCacheSys uint64

    // BuckHashSys 是用来 profiling bucket hash tables 的内存量(bytes)。
    BuckHashSys uint64

    // GCSys 是在垃圾回收中使用的 metadata 的内存量(bytes)。 
    GCSys uint64

    // OtherSys 是各种各样的 runtime 分配的堆外内存量(bytes)。
    OtherSys uint64

    // 垃圾回收统计。

    // NextGC 是下一次 GC 循环的目标堆内存尺寸。
    //
    // 垃圾回收器的目标是保持 HeapAlloc ≤ NextGC。
    // 在每一轮 GC 循环末尾,下一次循环的目标值会基于当前可达对象数据量以及 GOGC 的值来进行计算。
    NextGC uint64

    // LastGC 是上一次垃圾回收完成的时间,其值为自 1970 年纸巾的 nanoseconds(UNIX epoch)。
    LastGC uint64

    // PauseTotalNs 是自程序启动开始,在 GC stop-the-world 中暂停的累积时长,以 nanoseconds 计数。
    //
    // 在一次 stop-the-world 暂停期间,所有的 goroutines 都会被暂停,仅垃圾回收器在运行。
    PauseTotalNs uint64

    // PauseNs 是最近的 GC stop-the-world 暂停耗时的环形缓冲区(以 nanoseconds 计数)。
    //
    // 最近一次的暂停耗时在 PauseNs[(NumGC+255)%256] 这个位置。
    // 通常来说,PauseNs[N%256] 记录着最近第 N%256th 次 GC 循环的暂停耗时。
    // 在每次 GC 循环中可能会有多次暂停;这是在一次循环中的所有暂停时长的总合。
    PauseNs [256]uint64

    // PauseEnd 是最近的 GC 暂停结束时间的环形缓冲区,其值为自 1970 年纸巾的 nanoseconds(UNIX epoch)。
    //
    // 这个缓冲区的填充方式和 PauseNs 是一致的。
    // 每次 GC 循环可能有多次暂停;这个缓冲区记录的是每个循环的最后一次暂停的结束时间。
    PauseEnd [256]uint64

    // NumGC 是完成过的 GC 循环的数量。
    NumGC uint32

    // NumForcedGC 是应用程序经由调用 GC 函数来强制发起的 GC 循环的数量。
    NumForcedGC uint32

    // GCCPUFraction 是自程序启动以来,应用程序的可用 CPU 时间被 GC 消耗的时长部分。
    //
    // GCCPUFraction 是一个 0 和 1 之间的数字,0 代表 GC 并没有消耗该应用程序的任何 CPU。
    // 一个应用程序的可用 CPU 时间定义为:自应用程序启动以来 GOMAXPROCS 的积分。
    // 举例来说,如果 GOMAXPROCS 是 2 且应用程序已经运行了 10 秒,那么"可用 CPU 时长"就是 20 秒。
    // GCCPUFraction 并未包含写屏障行为消耗的 CPU 时长。
    //
    // 该值和经由 GODEBUG=gctrace=1 报告出来的 CPU 时长是一致的。 
    GCCPUFraction float64

    // EnableGC 显示 GC 是否被启用了。该值永远为真,即便 GOGC=off 被启用。
    EnableGC bool

    // DebugGC 目前并未被使用。
    DebugGC bool

    // BySize 汇报了按大小划分的 span 级别内存分配统计信息。
    //
    // BySize[N] 给出了尺寸 S 对象的内存分配统计信息,尺寸大小是:
    // BySize[N-1].Size < S ≤ BySize[N].Size。
    //
    // 这个结构里的数据并未汇报尺寸大于 BySize[60].Size 的内存分配数据。
    BySize [61]struct {
        // Size 是当前尺寸级别可容纳的最大对象的 byte 大小。
        Size uint32

        // Mallocs 是分配到这个尺寸级别的堆内存对象的累积数量。
        // 累积分配的内存容量(bytes)可用:Size*Mallocs 进行计算。
        // 当前尺寸级别内存活的对象数量可以用 Mallocs - Frees 进行计算。
        Mallocs uint64

        // Frees 是当前尺寸级别累积释放的堆内存对象的数量。
        Frees uint64
    }
}

推荐阅读更多精彩内容

  • Go语言内置运行时(就是runtime),抛弃了传统的内存分配方式,改为自主管理。这样可以自主地实现更好的内存使用...
    ddu_sw阅读 794评论 0 5
  • Go语言——内存管理 参考: 图解 TCMalloc Golang 内存管理 Go 内存管理 问题 内存碎片:避免...
    陈先生_9e91阅读 3,553评论 0 10
  • 介绍 了解操作系统对内存的管理机制后,现在可以去看下 Go 语言是如何利用底层的这些特性来优化内存的。Go 的内存...
    达菲格阅读 4,574评论 2 38
  • 前一篇讲了Go的调度机制和相关源码,这里说一下内存的管理,代码片段也都是基于Go 1.12。 简要的背景 一个程序...
    EagleChan阅读 1,190评论 0 2
  • 本文翻译自Memory Management in Go,介绍了Go语言中内存管理的相关概念。 所有的计算机程序语...
    绝望的祖父阅读 482评论 0 2