2021-11-05-golang内存分配

简介

最近被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 循环。

  2. 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信息。

如何阅读heap信息

golang中的MemStats数据结构是用于描述当前进程的内存消耗值。

概要统计信息

Alloc 分配给heap对象的内存大小。这个值和HeapAlloc相同。

TotalAlloc 累计分配给heap对象的内存大小。它的增长和Alloc是一样的,但是不会随着对象释放而降低。

Sys 从OS获取的全部内存大小。这个值就是下面XSys值的总和。Sys度量被Go的运行时使用的一些虚拟地址空间,用于存储heap,stacks,和其他的内部数据结构。 虚拟内存不会真实的分配物理内存,虽然通常这一切是在某个时刻发生。

Lookups 处理runtime时候使用的指针数量。通常用于运行时调试之用。

Mallocs 累计分配heap对象个数。还存活的对象数量就是Mallocs - Frees

Frees 累计释放heap对象个数。

堆统计信息
简介信息

介绍一些Go如何组织内存。Go分割heap的虚拟内存到spans,这种内存是连续区域的内存大小在8k或者更加大。一个span可能有下列3中状态:

idle容器中没有任何的对象或者数据。idle span背后的物理内存能被释放还给OS(但是虚拟地址空间并不会这么做),或者能被转换成in use或者stack span。

in use至少含有一个heap对象而且可能含有空闲的空间提供分配给heap对象。

stack span 提供给了goroutine 栈使用。栈span不会被视为堆的一部分。span可以被当成heap或者stack内存;但是不可能同时用于两种。

细则

HeapAlloc heap对象分配时使用的内存大小。分配的heap对象包含一切能被引用的的对象,也包含没有被引用还没有被GC回收的对象。特别的,HeapAlloc增长时伴随着heap对象的分配,降低时伴随heap内存清理和没被引用的对象被回收。清理渐进的发生于GC周期,所以二者处理都是同时在进行中,最终HeapAlloc的结果趋近于平滑(于之形成对比的锯齿的情况,发生在stop-the-world内存回收过程)。

HeapSys heap对象从OS分配内存空间。HeapSys度量的是heap使用的虚拟内存空间。这些虚拟机空间已经被分配,但是没有使用,这样的情况将不会消耗任何的物理内存,但是它变成未使用时(阅读等下将提到的HeapReleased量),当值将会变小,这种情况发生在虚拟内存对应的物理内存归还给OS。

HeapIdle idle spans的空间。里面没有任何的对象,这些span可以(或者已经)被归还给OS,或者他们可能被提供给heap分配器接着来分配,或者也可以提供给stack做内存。HeapIdle减去HeapReleased就等于将会归还给OS的空间,但是这块空间可能被runtime利用,以便于heap变大时候,无需再次从OS中申请更多内存。如果这个差值大于heap大小,预示着最近有一次瞬时的实时堆大小达到的峰值。

HeapInuse in-usespans占用的空间大小。至少有一个对象在这里。这种span只能被用于相同的内存尺寸对象的分配。HeapInuse-HeapAlloc等于当前还可以分配给特定尺寸对象内存量,但目前尚未使用到。这个是内存碎片的上限,但是通常这么做能更加高效的复用。

HeapReleased 归还给操作系统的物理内存。从idle spans归还给操作系统的总数内存块,尚未为堆重新获取。

HeapObjects 当前分配的heap对象数目。和HeapAlloc相似,此值增减发生在对象分配和堆整理而且不可达对象的清理。

栈信息

堆和栈内存是互相独立的,在go运行时中,能将二者的内存互相转换。

细则

StackInuse 栈spans中使用全部内存块

StackSys 栈从OS中获取的内存块。StackSys是StackInuse加上少量直接从系统获取的空间为系统级线程的栈。

非堆栈内存信息

下面部分是运行时内部的数据结构的内存使用情况,未从堆内存分配的结构(通常是因为它们是实现堆的一部分)。不像堆或者栈内存,这种内存有专属的数据结构。

这些内存主要用于调试运行时内存。

细则

MSpanInuse / MSpanSys 分配mspan的内存,从系统中获取的空间。

MCacheInuse / MCacheSys 分配mcache的内存,从系统中获取的空间。

BuckHashSys 分析hash桶表大小。

GCSys gc元数据。

OtherSys 杂项。

为了方便,我写了一个读取的脚本。源码下载地址

这里我分析了一份内存:

概要信息
------------------
堆使用空间: 1.2G
累计分配堆空间: 10.3G
从系统分配的虚存: 0B
堆累计分配obj个数: 228.4M
堆累计归还obj个数: 213.1M
存活obj个数: 15.3M


堆信息
------------------
堆span空间中被obj用的空间: 1.2G
堆span累计空间: 1.8G
堆空闲span空间: 428.6M
堆span的全部空间: 1.4G
堆span中obj碎片空间: 188.1M
从堆span归还给OS空间: 1000.0K
当前内存中obj个数: 15.3M
全部堆内存使用: 1.8G


栈信息
------------------
栈spans中使用全部内存块: 1.3M
栈从OS中获取的内存块: 1.3M


非堆栈内存
------------------
mspan使用内存: 23.5M
mspan从OS中获取的内存块: 30.2M
mcache使用内存: 4.7K
mcache从OS中获取的内存块: 16.0K
分析hash桶表大小: 1.6M
gc元数据: 81.9M
杂项: 4.2M

heap分布I3FDiD.png

引用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值