Abel'Blog

我干了什么?究竟拿了时间换了什么?

0%

go-内存分配

简介

最近被golang的内存问题困扰,这里做一下功课,将通过收集资料,记录笔记的方式将golang的内存分配相关知识搞清楚。资料来自于阅读《go语言学习笔记》,源码,网上的资料。

大事件

2014/06 go 1.3: 并发清理
2015/08 go 1.5: 三色并发标记

基础数据结构、概念

STW(Stop The Word)

在垃圾回收算法中,Stop The Word(STW)是一个很重要的概念,他会中断程序运行,添加写屏障,以便扫描内存 ,现在一起来看看它内部的原理以及可能存在的问题。并行清理是指垃圾回收和用户逻辑并发执行。

流程

golang源码注释

mgc说明

gc源码位置:Go\src\runtime\mgc.go。在它的源码的注释里面写了大段关于内存回收相关知识。下面是翻译。

点击下载

GC 与 mutator 线程并发运行,类型准确(又名精确),允许多个GC 线程并行运行。 它是使用写屏障的并发标记和清除。 这是非分代和非压缩。 分配是使用每个 P 分配隔离的大小完成的在常见情况下消除锁定的同时最小化碎片的区域。

算法分解为几个步骤。

这是正在使用的算法的高级描述。 对于 GC 的概述,一个很好的入门是 Richard Jones 的 gchandbook

  1. GC 执行扫描终止。

    a. 停止世界。 这会导致所有 P 达到 GC 安全点。

    b. 扫描任何未扫描的跨度。 只有在以下情况下才会有未扫过的跨度在预期时间之前强制执行此 GC 循环。

    1. GC 执行标记阶段。

      a. 通过将 gcphase 设置为 _GCmark 来准备标记阶段
      (来自_GCoff),启用写屏障,启用mutator
      协助和排队根标记作业。没有对象可以
      扫描直到所有 Ps 都启用了写屏障,即
      使用 STW 完成。

    b. 开始世界。从这点来说,GC工作由mark来完成
    由调度程序启动的工作人员和由执行的协助
    部分分配。写屏障遮蔽了
    覆盖指针和任何指针的新指针值
    写入(有关详细信息,请参阅 mbarrier.go)。新分配的对象
    立即被标记为黑色。

    c. GC 执行根标记作业。这包括扫描所有
    堆栈,对所有全局变量进行着色,并对其中的任何堆指针进行着色
    堆外运行时数据结构。扫描堆栈停止
    goroutine,将在其堆栈中找到的任何指针着色,然后
    恢复 goroutine。

    d. GC 排空灰色对象的工作队列,扫描每个灰色对象
    将对象变为黑色并对在对象中找到的所有指针进行着色
    (反过来可能会将这些指针添加到工作队列)。

    e.因为 GC 工作分布在本地缓存中,所以 GC 使用
    分布式终止算法来检测何时没有
    更多根标记作业或灰色对象(参见 gcMarkDone)。在这
    点,GC 过渡到标记终止。

    1. GC 执行标记终止。

      a. 停止世界。

      b. 将 gcphase 设置为 _GCmarktermination,并禁用 worker 和 辅助。

      c.执行内务处理,如刷新 mcaches。

    2. GC 执行扫描阶段。

      a. 通过将 gcphase 设置为 _GCoff 来准备扫描阶段, 设置扫描状态并禁用写屏障。

    b. 开始世界。从现在开始,新分配的对象
    是白色的,如有必要,在使用前分配扫描跨度。

    c. GC 在后台和响应中进行并发扫描
    分配。请参阅下面的说明。

    1. 当足够的分配发生时,重放序列
      从上面的 1 开始。请参阅下面对 GC 率的讨论。

    并发扫描。

    扫描阶段与正常程序执行同时进行。
    堆被一个跨跨度地懒惰地扫过(当一个 goroutine 需要另一个跨度时)
    并在后台 goroutine 中并发(这有助于不受 CPU 限制的程序)。
    在 STW 标记终止结束时,所有跨度都被标记为“需要清除”。

    后台清扫器 goroutine 只是一一扫过 span。

    为了避免在有未扫描跨度时请求更多操作系统内存,当
    goroutine 需要另一个跨度,它首先尝试回收那么多内存
    通过清扫。当一个 goroutine 需要分配一个新的小对象跨度时,它
    扫描相同对象大小的小对象跨度,直到它至少释放
    一个对象。当一个 goroutine 需要从堆中分配大对象跨度时,
    它会扫描 span,直到将至少那么多页释放到堆中。有
    这可能不够的一种情况:如果一个 goroutine 清除并释放两个
    不相邻的一页跨越到堆,它将分配一个新的两页
    跨度,但仍然可以有其他一页未扫过的跨度
    合并成一个两页的跨度。

    确保没有操作在未扫描的跨度上继续(这会破坏 在 GC 位图中标记位)。在 GC 期间,所有 mcache 都刷新到中央缓存中, 所以它们是空的。当一个 goroutine 将一个新的跨度抓取到 mcache 中时,它会清除它。 当一个 goroutine 显式地释放一个对象或设置一个终结器时,它确保 扫描跨度(通过扫描它或等待并发扫描完成)。 只有当所有跨度都被扫描时,终结器 goroutine 才会启动。 当下一次 GC 开始时,它会扫描所有尚未扫描的跨度(如果有)。

    垃圾回收率。

    下一次 GC 是在我们分配了与已经使用的数量。比例由GOGC环境变量控制(默认为 100)。如果 GOGC=100 并且我们使用的是 4M,当我们达到 8M 时我们会再次 GC(此标记在 gcController.heapGoal 变量中跟踪)。这将 GC 成本保持在与分配成本成线性比例。调整GOGC只是改变线性常数(以及使用的额外内存量)。

    小物件

    为了防止在扫描大对象时出现长时间的停顿并
    提高并行性,垃圾收集器将扫描作业分解为
    大于 maxObletBytes 的对象最多变成“oblets”
    maxObletBytes.当扫描遇到大的开头
    对象,它只扫描第一个 oblet 并将剩余的排入队列
    oblets 作为新的扫描作业。

mstats说明

内存分配

制作了预分配、内存池操作。

分配大概流程

内存分配只关心内存块,不关心对象状态。不会主动发起内存回收,垃圾回收器完成清理工作后,触发内存分配器的回收操作。

  1. 每次从操作系统申请大块内存,减少系统调用;
  2. 将内存块按照预先的大小切分成小块,够造成链表;
  3. 分配对象内存的时候,从大小合适的链表中提取一块小块使用;
  4. 回收对象内存时,将此对象重新归还到合适的链表中;
  5. 如果闲置内存过多,尝试将归还部分内存给操作系统;

内存块

分配器只有两种逻辑块。

span:由多个地址连续的页(page)组成的大块内存。
object:将span按照特定大小切分成小块,用于保存object。

按照用途来分,span是提供分配器内部使用,object是面向外部分配提供的。

span中的size也会更具实际情况做裁剪,或者归还给系统。对应的代码位置:malloc.gomheap.go

存储object的空间按照8字节对齐。底层制作了分配class的映射表,超过32kb的object将会当成 large object 对待。

三色标记和写屏障

  1. 开始全部为白色;
  2. 扫描出全部可达对象,标为灰色,写入待处理队列;
  3. 从队列提取会随对象,将其引用对象标记灰色,放入队列,自己标记为黑色;
  4. 写屏障监视对象内存修改,重新标色或者放回对了;
  5. 完成上述工作后,只有黑白两色,黑色全部回收。

控制器 gcControoler

控制器会参与回收任务,记录状态数据,动态调整运行策略,影响并发标记单元的工作模式和数量,平衡CPU资源占用。回收结束之后,参与next_gc回收阈值设置,调整垃圾回收出发频率。

测试数据

测试两份内存web数据,heap的dump信息。

引用

[1] Golang runtime内存管理机制

[2] go怎样做stw

[3] Go语言设计与实现

[4] GoGC优化