GO内存管理 | 青训营笔记

62 阅读5分钟

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

GC的作用

  • 避免手动管理内存
  • 保证程序执行时内存的安全性和正确性

GC的三个任务

  • 分配新对象的内存空间
  • 找到存活的对象
  • 释放死亡对象的内存空间

GC相关名词概念

  • Mutator:业务线程,用于创建新对象,并修改对象的引用关系

  • Collector:GC线程,用于回收死亡对象的内存和找到存活的对象

  • Serial GC:只有一个Collector线程

    image.png

  • Parallel GC:有多个Colleator线程执行GC操作

    image.png

  • Concurrent GC:mutator(s)和collector(s)可以同时执行

    image.png

Concurrent GC必须要感知对象指向关系的改变,不然GC可能会出错,例如下图:

image.png

在GC过程中mutator业务线程创建了对象b,这个对象被o指向,如果不给b对象标记存活,那么b会在gc过程中被回收,出现gc错误!

评价GC算法的几个标准

  • 安全性:不能回收存活的对象
  • 吞吐率:1-gc使用时间/程序执行总时间(愈高愈好)
  • 暂停时间:stop the world是否影响业务
  • 内存开销

追踪垃圾回收

  • 初始化标记:全局变量、栈、常量、静态变量等

  • 标记可达对象:计算传递闭包,找到所有可以到达的对象

  • 清理:处理剩下的不可达的对象

    • 清理方式
      • Copying GC:开辟一块新的内存空间,将存活的对象复制过去

        image.png

      • Mark-Compact GC:移动存活的对象到内存空间的开头

        image.png

      • Mark-sweep GC:使用free-list连接所有空闲内存,将释放的内存连接到free-list上

        image.png

清理策略(Generational GC,分代GC)

  • 分代假说:大部分对象分配出来没多久就死亡
  • 对象年龄:经历的GC次数越多年龄越大
  • 将堆内存分为年轻区(young generation)和老年区(old generation),年轻区在内存头,老年区在内存尾
  • 年轻区采用copying gc
  • 老年区采用Mark-sweep gc

引用计数

  • 对象被引用,计数加一
  • 计数=0清除对象
  • 优点
    • 将gc放到程序执行中进行
    • 内存管理不需要知道runtime的细节
  • 缺点
    • 环形数据结构无法清除,每一个对象的计数都不为0
    • 可能会引起暂停
    • 较大的内存开销
    • 可能有多个线程引用同个对象,计数器增加减少需要原子操作,维护计数器的开销较大

GO内存分配机制

分块机制

  • 先调用mmap系统调用申请一大块内存空间
  • 之后调用mspan在内存空间切分成大块
  • 在大块中分出小块,对象分配在小块中
  • mspan类别
    • noscan mspan:分配的对象包含指针,GC不需要扫描
    • scan mspan:分配的对象不包含指针,GC需要扫描

image.png

例如上图中8KB是mspan分出来的大块,每个大块中的8B、16B是小块

缓存机制

缓存过程图如下:

image.png

  • GO内存分配借鉴了TCMalloc:Thread Caching Malloc
  • 每一个p包含一个mcache用于分配内存,用于为绑定的g分配内存
  • mcache管理一组mspan
  • 首先在mspans中查找空闲的小块,如果没有空闲mspan,就去mcentral中找空闲的mspan
  • mcentral中mspan空闲并不会立即释放并归还OS,而是缓存在mcentral中

优化

GO语言内存分配特点:

  • GO语言分配对象路线很长,g->m->p->mcache->mspan->memory block->return pointer
  • 对象分配占用的CPU时间比较长
  • 小对象分配居多

优化策略:采用Balanced GC

Balanced GC

  • 给每一个g单独分配一个GAB(goroutine allocation buffer),大小为1KB
  • 在goroutine申请内存的时候只需要在GAB上分配即可
  • GAB上有三个指针base、top、end,分配的时候移动top指针并检验是否越界即可

GAB图例:

image.png

分配算法:

image.png

GAB对于GO内存管理来说就是一个大对象,本质其实是将小对象的分配合并;GAB可能会导致内存的延迟释放,因为GAB内只要有一个对象存活,GAB就不会被释放。

可以采用survivor GAB来存储存活下来的对象,使用copying gc算法管理这些小对象。

image.png

编译器优化

编译器编译流程:

image.png

IR:Intermediate Representation中间表示

静态分析

不执行程序,分析程序的执行过程,可以得到数据流和控制流

  • 控制流:程序执行的流程
  • 数据流:数据在控制流上的传递
  • 通过数据流,可以得到更多关于程序的性质,可能可以优化程序

过程内分析和过程间分析

  • 过程内分析:在函数内部分析代码优化
  • 过程间分析:函数内有函数调用,考虑函数调用时的参数传递和返回值的数据流和控制流
    • 过程间分析需要同时分析数据流和控制流

GO编译器特点

为了减少编译时间,不会做过多的代码优化;自带函数内联优化,但是条件很严格,大部分场景不会优化函数内联;

函数内联:

  • 优点:
    • 可以减少运行时间(减少了函数调用)
    • 逃逸分析中,逃逸的指针变少
  • 缺点:
    • GO镜像变大
    • GO函数体过长,对instruction cache不友好

优化策略:Beast Mode

Beast Mode

放宽了函数内联的限制条件,让更多函数可以内联,函数体超出限制条件长度之后不内联。内联之后做了新的逃逸分析,对于没有逃逸的指针,将其分配在栈内存中。