Go内存管理 | 青训营笔记

60 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天

自动内存管理

概念

  • 动态内存
    • 程序在运行时根据需求动态分配的内存:malloc()
  • 垃圾回收:有程序语言的运行时系统管理动态内存。
    • 避免手动回收,专注业务实现
    • 保证内存是安全性和正确性。
  • Mutator:业务线程,分配新对象,修改对象指向关系,也就是用户启动的线程。
  • Collector:GC线程,找到存活对象,回收垃圾对象。
  • Seruak GC: 只有一个collector。
  • Parallel GC: 支持多个collectors 同时回收的GC算法。
  • Concurrent GC: mutator(s) 和 collector(s) 可以同时执行。

image.png

Concurrent GC 遇到的挑战

image.png

Concurrent GC 是并发标记,标记的时候用户线程和 GC 线程同时进行。

所以当回收时遇到以下图中情况,第一张图是GC前,O、a都没有被标记。第二张图,Collectors 先将 O、a 标记为存活。

但这时,GC 还没结束,用户就添加了一个新的引用关系 o 引用 b。

这时Collectors 已经标记完 O、a 这一串,但是必须感知到新增的引用关系,去处理它,不能漏掉对它的GC。

GC 的三个任务

  • 为新对象分配空间
  • 找到存活对象
  • 回收垃圾对象

GC 的评价标准

  • 安全性
    • 不能回收存活的对象
  • 吞吐率
    • 1 - GC时间/程序执行总时间
  • 暂停时间
    • 业务是否感知到
  • 内存开销
    • GC内存开销。

常见方式

追踪垃圾回收算法

回收对象

指针指向关系不可达对象

流程

  • 标记根对象
    • 常量、变量、静态变量、常量。
  • 找到可达对象
  • 清理不可达对象
    • 标记整理
    • 标记清除
    • 标记复制

怎么选择合适的回收算法

根据生命周期来选择

  • 老年代使用标记-整理算法。

  • 新生代使用标记-清除 + 标记-整理算法。

引用计数垃圾回收算法

回收对象

  • 被引用数不大于0的对象

优点

  • 垃圾回收操作平摊到程序执行过程中,不像追踪垃圾回收算法,每次回收时才扫描一遍,引用计数回收算法,在程序运行时,就维护着计数。
  • 内存管理不需要了解 runtime 的实现细节,维护计数就行,比如C++智能指针,一个库就能实现垃圾回收。

缺点

  • 引用计数统计需要具备原子性,开支大
  • 没办法回收环形数据结构
  • 每个对象都要开辟空间,用来计数
  • 回收时依然有stopworld

Go内存管理及优化

内存分配策略

提前将内存分区

image.png

  • 调用系统调用 mmap() 分配一块4 MB 的内存。
  • 将内存分大块,每一块 8KB,称作 mspan。
  • 再将大块划分成特定大小的小块,用于对象分配。

最后将 mspan 划分为两类

  • noscan mspan: 分配不包含指针的对象 -- GC 不用扫描
  • scan mspan: 分配包含指针的对象 -- GC 需要扫描

分配

根据对象的大小,找到最合适的直接分配。

内存分配 --多级缓存

简介

go 实现了内存分配的多级缓存机制。

对于没有对象的 mspan(空闲内存),不会立即回收,而是进行缓存,从而加快内存分配。

原理

image.png

  1. 图中 g 指的是 goroutine,每个 p 都指向一个 mcache 来为 groutine 分配内存。
  2. 当分配对象时,首先在 mache 中查找有无合适的 mspan。
  3. 找不到合适的 mspan(或者 mspan 都满了),就去查询下级 mcentral 有无合适的 mspan,有的话,直接拿到 mcache 中使用。
    1. 当 mspan 没有分配的对象,msapn 会被缓存在 mcentral 中,而不是立刻释放。
    2. 这就尽可能保证 mcentral 中尽量有空闲内存。

内存管理优化

问题

  • 对象分配是高频操作:每秒分配GB级别内存。
  • 小对象占比高
  • Go 内存分配比较耗时
    • 分配链路长: g -> m -> p -> mcache -> mspan -> memory block -> return pointer。
    • pprof: 对象分配中调用最频繁的函数。

字节优化方案:Balanced GC

简介

为每个 goroutine 绑定一个 1kb 的空间(GAB)(goroutine allocation buffer)。

并规定小于 128b 的小对象都在这个 GAB 中直接分配。

当 GAB 用完后,通过 mcache 再次申请内存。

这样就把小对象内存分配转化成了大对象内存分配,极大的提高效率。

GAB 内存分配算法

image.png

GAB 中有 base 指针指向 GAB 起始位置,end 指针指向 GAB 结束位置。

top 指针从 base 开始,已分配内存与未分配内存区域的边界线。

GAB 中分配内存只需要移动 top 指针就好。

简单高效。

优点

  • GAB 是线程独有的,不需要考虑并发问题。
  • 分配动作(移动top指针)简单高效。

缺点

描述

  • GAB 内部小对象存活,导致整个 GAB 大对象无法回收。
    • Go 采用的引用追踪内存回收算法,当小对象存活时,会依赖于 GAB 大对象,导致整个 GAB 无法被回收。

解决方案

  • (标记-复制)当 GAB 中对象总大小到某个阈值的时候,分析存活对象,并将存活对象复制到新的 GAB 中,

Balanced GC 收益

高峰期 CPU useage 降低 4.6%,核心接口时延下降 4.5% - 7.7%。