内存管理与优化 | 青训营笔记

111 阅读2分钟

内存管理

第一个思想:分配

  • 目标:为对象在heap上分配

  • go的做法:提前将内存分块,要用的时候填入尺寸最接近的块

  • 具体做法

    • 系统调用 mmap() 向 OS申请了一大块内存(例如4mb)

    • 先将内存划分成大块,例如 8kb ,称为 mspan

    • 在将大块继续划分成特定大小的小块,用于对象分配

    • 分类

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

第二个思想:缓存

  • 通过 TCMalloc(thread caching) 内存分配器来分配
  • 每个 p 包含一个 mcache 用于快速分配,用于为绑定于 p 上的 g 分配对象
  • gorutine 出发找到m后找到p,在p上的 mcache 中存放了一组 mspan,每个 mspan 的大小是不一样的,根据对象大小找到最合适的 mspan 中的空位后返回出去,就完成了一次对象的分配
  • 当我们发现 mcache 中的 mspans 都是满的,我们就需要到下一个级别的缓存 mcentral 中找一个带有空余对象的 mspan,将此 mspan 填入 mcache,再将这个 mspan 中的一个空位返回出去
  • 结论:go做了多层缓存让我们更快地将对象分配出去
  • 当 mspan 中没有对象时,mspan 会被缓存在 mcentral 中,而不是立刻释放并归还给 OS
  • image-20230121222735394

优化

  • 对象分配是非常高频的操作,每秒都能分配几个G的内存

  • 小对象的占比更高

  • Go 内存分配比较耗时

    • 分配路径长:g -> m -> p -> mcache -> mspan -> memory block -> return pointer
    • pprof:对象分配的函数是最频繁调用的函数之一(cpu消耗高)

优化方案:Balanced GC

  • 每个 g 都绑定一大块内存(1kb),称为 goroutine allocaton buffer(GAB)

  • GAB 用于 noscan 类型的小对象分配: <128 B

  • 使用三个指针维护 GAB :base,end,top

  • 使用 Bump pointer(指针碰撞)风格对象分配

    • 无需和其他分配请求互斥
    • 分配动作简单高效
  • image-20230121223050347

  • 技术细节:

    • GAB 对于 Go 内存管理来说是一个大对象

    • 本质:将多个小对象的分配合并成一个大对象的分配

    • 问题:GAB 的对象分配方式会导致内存被延迟释放(一个小对象的存活会导致一个大对象一直存活,浪费内存资源)

    • 方案:移动 GAB 中的存活对象

      • 当 GAB 的总大小超过阈值时,将 GAB 中存活的对象复制到另外分配的 GAB 中
      • 原先的 GAB 可以释放,避免内存泄漏
      • 本质:用 copying GC 的算法对内存进行清理