「5」性能调优|GC & Golang GC 调优|青训营笔记

412 阅读4分钟

创建时间: May 17, 2022 5:10 PM 标签: GC, 内存优化

这是我参与「第三届青训营 -后端场」笔记创作活动的的第6篇笔记。

性能优化的定义见上节,略。

GC

GC:(garbage collection) 1、为新的对象分配内存。 2、找到存活对象。 3、回收死亡对象。 其意义在于不用编码人员不用管理内存生命周期,解决 double-free、use-after-free 两大问题

GC 算法的类别

  • SerialGC 暂停业务 用一个回收器回收
  • ParallelGC 暂停业务 用多个回收器回收
  • ConcurrentGC 并行GC 过程中业务受影响小

GC 算法的评价标准

  1. 安全性

    1. 不能回收存活对象
    2. GC 算法的基本要求
  2. 吞吐率

    1. GC 时间占总时间的比率
  3. 暂停时间

    1. STW 时间,GC 过程是否影响业务
  4. 内存开销

    1. GC 的元数据开销

评价 GC 合规的标准,是否”安全“,开销可以接受。

追踪垃圾回收

GC 的最简单实现:SerialGC 暂停业务,从根对象遍历,所有达对象保留,不可达对象直接回收。

  • 根对象:静态变量、全局变量、常量、线程栈等
  • 可达对象:从根对象求指针引用图,图上的所有节点视为”可达
  • 清理对象:所有不可达对象

清理策略

多个较小对象可能被分配到同一个内存块中,某些小对象将要被清除时将执行的逻辑被称作清理策略。

  • 复制 - Coping GC

    • 将存活对象从原本的内存块中复制出来,重新分配,放到新的内存空间中。
  • 标记 - Mark-sweep GC

    • 标记&视作清除,对内存块中的不可达部分进行标记,不立刻执行清除,直到该部分内存空间需要被分配给新的对象。
    • 避免对象复制。
  • 压缩 - Mark-compact GC

    • 移动&整理,将内存块中的不可达对象清除,存活对象就地压缩到内存块头部。

各策略适用场景稍后有整理,部分内容可能有疏漏欢迎读者补充。

分代 GC

  • 对象的年龄:对象从被创建出来之后经历 GC 并存活的次数,类似树的年轮?

  • 分代的目的:对不同分代的对象制定不同的 GC 策略,降低 GC 开销。

    • 不同代的对象处于不同的堆空间,比如 JVM

分代 GC 策略

  • 年轻代

    • 属于常规的对象分配
    • 由于这部分对象存活很少,可以使用 Coping GC 策略
    • GC 吞吐率高
  • 老年代

    • 对象趋向于一直活着,每次 GC 这部分对象都存活,该对象被反复复制
    • 可以采用 Mark-sweep GC 策略

引用计数

!! 个人理解可能非常不准确:引用计数可以减轻部分 GC 压力,代价是付出额外的内存空间。如果回收超大对象时,仍然会触发 STW,暂停业务。

引用计数

  • 每个对象自身维持一个计数,用于表示与自己关联的引用数目。

  • 对象存活:当且仅当引用数 > 0。

  • 优点:

    • 内存管理操作被平摊到程序的执行过程中。
  • 缺点:

    • 可能存在孤岛的互相引用对象。

内存优化

Golang 内存模型

为对象在进程的堆空间上分配内存

内存分块

  • syscall mmap 向 OS 申请大块内存 (比如 4MB
  • 将内存划分为大块 mspan (比如 8KB
  • 再将一个 mspan 划分为特定大小的块,用于合适对象的内存分配。
  • noscan mspan:分配不包含指针的对象——GC 不扫描该内存 ( 非逃逸,视作栈空间
  • scan mspan:分配包含指针的对象——GC 需要扫描

对象分配:

根据对象大小,匹配最合适的内存块。

内存分配部分总结:对整块内存申请,按使用量进行分配。

GBA 方案(goroutine allocation buffer)

为每个 g 绑定一块内存(1KB),用于分配 noscan 类型的小对象 < 128B。

优化方案来自数据统计:业务项目中该大小区间被创建的对象非常多 使用三个指针维护 base、end、top,利用 P 独享性质,完成无须互斥的内存分配(指针碰撞)。

代价:增加了内存延迟释放的问题?

解决方案:当 GBA 总大小超过一定阈值,将存活对象复制到另分配的 GBA 对象中(合并),释放原本 GBA 所占的内存空间。

本质:用 copyingGC 算法管理小对象

什么叫延迟释放?

对象如果未被引用则在下一次 GC 时内存空间会被回收,但引入 GBA 后,如果 GBA 中有小对象仍然存活,则整个 GBA 所占内存空间不会被立即释放,需等该小对象生命周期结束,这样的问题成为延迟释放。