golang gc

The Go Malloc

Golang运行时的内存分配算法为 C 语言开发的TCMalloc算法,全称Thread-Caching Malloc。为了降低锁粒度把内存分为多级管理。
Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。

未命名文件.png

arena区域就是我们所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成8KB大小的页,一些页组合起来称为mspan。
bitmap区域标识arena区域哪些地址保存了对象,并且用4bit标志位表示对象是否包含指针、GC标记信息。bitmap中一个byte大小的内存对应arena区域中4个指针大小(指针大小为 8B )的内存,所以bitmap区域的大小是512GB/(48B)=16GB。
spans区域存放mspan(也就是一些arena分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以spans区域的大小就是512GB/8KB
8B=512MB。除以8KB是计算arena区域的页数,而最后乘以8是计算spans区域所有指针的大小。创建mspan的时候,按页填充对应的spans区域,在回收object时,根据地址很容易就能找到它所属的mspan。

需要知道的重点结论

同一进程的所有线程共享相同的内存空间,他们申请内存时需要加锁,如果不加锁就存在同一块内存被 2 个线程同时访问的问题。

TCMalloc 的做法是什么呢?为每个线程预分配一块缓存,线程申请小内存时,可以从缓存分配内存,这样有 2 个好处:

  1. 为线程预分配缓存需要进行 1 次系统调用,后续线程申请小内存时,从缓存分配,都是在用户态执行,没有系统调用,缩短了内存总体的分配和释放时间,这是快速分配内存的第二个层次。
  2. 多个线程同时申请小内存时,从各自的缓存分配,访问的是不同的地址空间,无需加锁,把内存并发访问的粒度进一步降低了,这是快速分配内存的第三个层次。

Thread Cache: 每个线程各自的 Cache,一个 Cache 包含多个空闲内存块链表,不同的链表内存块大小也不同,程序根据需要到对应的链表申请内存。

Stack
  • 和堆一样存储在计算机 RAM 中。
  • 在栈上创建变量的时候会扩展,并且会自动回收。
  • 相比堆而言在栈上分配要快的多。
  • 用数据结构中的栈实现。
  • 存储局部数据,返回地址,用做参数传递。
  • 当用栈过多时可导致栈溢出(无穷次(大量的)的递归调用,或者大量的内存分配)stack overflow。
  • 在栈上的数据可以直接访问(不是非要使用指针访问)。
  • 如果你在编译之前精确的知道你需要分配数据的大小并且不是太大的时候,可以使用栈。
  • 当你程序启动时决定栈的容量上限。
Heap
  • 和栈一样存储在计算机RAM。
  • 在堆上的变量必须要手动释放,不存在作用域的问题。数据可用 delete, delete[] 或者 free 来释放。
  • 相比在栈上分配内存要慢。
  • 通过程序按需分配。
  • 在堆上创建数的据使用指针访问,用 new 或者 malloc 分配内存。
  • 如果申请的缓冲区过大的话,可能申请失败, out of memory。
  • 可能造成内存泄露, memory leak。

再谈GC

gc 更多是针对堆上面的对象进行回收释放

一个标准进程的内存模型

  • code area(方法区)
  • static area 静态变量
  • heap (堆)
  • stack (栈)
image.png

stack会随着线程执行code area的stack frame,自动pop、push、remove
stack里的变量、调用完毕,就随着出栈,自动销毁。
但是heap不会,heap内的对象,通常是用stack内或者其它heap对象内的指针变量,对heap内的对象进行操作。
这个也叫做对象引用。
垃圾回收,garbage collect,回收的就是heap的空间。

所有的 GC 算法其存在形式可以归结为追踪(Tracing)和引用计数(Reference Counting)这两种形式的混合运用。

  • 追踪式 GC

    从根对象出发,根据对象之间的引用信息,一步步推进直到扫描完毕整个堆并确定需要保留的对象,从而回收所有可回收的对象。Go、V8 对 JavaScript 的实现等均为追踪式 GC。

标记 mark:从根对象开始通过 DFS 查询到所有引用的对象,标记为有颜色(说明这些对象都是有用的)

清除 sweep:回收未被标记的垃圾对象并将回收的内存加入空闲链表;

  • 引用计数式 GC

    每个对象自身包含一个被引用的计数器,当计数器归零时自动得到回收。因为此方法缺陷较多,在追求高性能时通常不被应用。Python(引用计数为主,标记清除为辅)、Objective-C、Java 等均为引用计数式 GC。

go gc发展史

  • Go 1:串行三色标记清扫
  • Go 1.3:并行清扫,标记过程需要 STW,停顿时间在约几百毫秒
  • Go 1.5:并发标记清扫,停顿时间在一百毫秒以内
  • Go 1.6:使用 bitmap 来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存,停顿时间在十毫秒以内
  • Go 1.7:停顿时间控制在两毫秒以内
  • Go 1.8:混合写屏障,停顿时间在半个毫秒左右
  • Go 1.9:彻底移除了栈的重扫描过程
  • Go 1.12:整合了两个阶段的 Mark Termination,但引入了一个严重的 GC Bug 至今未修(见问题 20),尚无该 Bug 对 GC 性能影响的报告
  • Go 1.13:着手解决向操作系统归还内存的,提出了新的 Scavenger
  • Go 1.14:替代了仅存活了一个版本的 scavenger,全新的页分配器,优化分配内存过程的速率与现有的扩展性问题,并引入了异步抢占,解决了由于密集循环导致的 STW 时间过长的问题

三色标记、混合写屏障

https://zhuanlan.zhihu.com/p/334999060

  • 插入屏障(堆)
    在A对象引用B对象的时候,B对象被标记为灰色
  • 删除屏障(堆)
    被删除的对象,如果自身为灰色或者白色,那么被标记为灰色
  • 混合写屏障(栈)
    1. GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)
    2. GC期间,任何在栈上创建的新对象,均为黑色。
    3. 被删除的对象标记为灰色。
    4. 被添加的对象标记为灰色。

你可能感兴趣的:(golang gc)