Go内存管理 | 青训营笔记

53 阅读4分钟

# Go内存管理 | 青训营笔记

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

从一个Javaer的角度来学习Go

基本概念

在了解Go的内存管理之前,我们需要了解一些基本概念。如果之前了解过JVM,这部分就相当于复习了

  • Mutator:业务线程
  • Collector:GC线程
  • Serial GC:只有一个collector
  • Parallel GC:支持多个collectors同时回收
  • Concurrent GC:业务线程和GC线程可以同时执行

如何评价GC算法

  1. 安全性(基本要求):不能回收存活的对象
  2. 吞吐量:1- GC时间/程序执行总时间
  3. 暂停时间(STW)
  4. 内存开销

如何判断对象是否是垃圾

引用计数法

给每个对象创建一个计数器,每当有地方引用它时,计数器加一,如果引用失效,计数器减一,当计数器为0时,认为是垃圾。

这种方法最大的缺点就是就是无法解决循环引用,比如A引用了B,B又引用了A,虽然没有其他地方引用这两个对象,但它们却无法被垃圾回收

追踪垃圾回收(可达性分析)

通过标记根对象,来判断哪些节点不可达,从而进行清除。这也是JVM采用的方法

GC算法

标记清除算法

先标记出所有需要回收的对象,之后再进行统一的回收

缺点:

  1. 如果有大量对象需要回收,那么就需要大量的标记和清除操作,时间效率低
  2. 容易产生内存碎片

标记复制算法

准备两个半区,首先只分配其中一个半区的空间,当需要垃圾回收时,将已使用半区的存活对象复制到未使用半区的空间,清除使用过的半区的空间

优点:

  1. 不会产生内存碎片
  2. 当剩余存活对象较少时,效率很高

缺点:

  1. 当剩余存活对象较多时,效率低

标记整理算法

为了解决标记清除算法的内存碎片问题,标记整理算法在标记之后,选择先让所有用户线程停止,然后将所有的存活对象向一端进行对齐,之后清除边界外的内存空间。

缺点:

  1. 虽然标记清除算法也需要Stop the world操作来进行标记,但标记整理算法的停顿时间较长

分代假说

因为有些对象被创建出来后很快就不被使用了,如果我们还是按照统一的频率和策略进行GC,那么性能就不会有很好的表现。因此,我们按照每个对象的年龄(经过GC的次数)进行划分,分为老年代和新生代。新生代可以采取标记-复制法,而老年代可以采取标记-清除法。

Go内存分配

  1. 提前将内存分块
    1. 通过mmap向OS申请一大块内存,例如4MB
    2. 先将内存划分成大块,称作mspan
    3. 再将大块划分成特定大小的小块,用于对象分配
    4. noscan mspan:分配不包含指针的对象——GC不需要扫描
    5. scan mspan:分配包含指针的对象——GC需要扫描
  2. 根据对象的大小,选择最合适的块返回

除此之外,为了goroutine快速分配对象,每个逻辑处理器都有自己的mcache,mcache管理着一组mspan,当mcache中的mspan分配完毕,会向mcentral申请带有未分配块的mspan,当mspan中没有分配的对象,mspan会被缓存在mcentral中,而不是立刻归还给OS

再次优化

目前字节内部对Go的内存分配也进行优化,其中一点为Balanced GC,即将多次小对象的分配合成为一次大对象的分配

对于每个goroutine,都绑定一块内存,称为goroutine allocate buffer(GAB),用于noscan类型的小对象分配,我们只需三个指针,就能维护这片内存的使用情况,即base,top,end image.png