1. GC 介绍

GC 是一种自动管理内存的技术,用来回收(释放) heap 中不再使用的对象。

GC 过程中涉及到两个阶段:

  1. 区分活对象(live object)与垃圾对象(garbage)

  2. 回收垃圾对象的内存,使得程序可以重复使用这些内存

1.1 追踪技术(Tracing)

这是目前使用范围最广的技术,一般我们提到 GC 都是指这类。

这类 GC 从某些被称为 root 的对象开始,不断追踪可以被引用到的对象,这些对象被称为可到达的(reachable),其他剩余的对象就被称为 garbage,并且会被释放。

1.1.1 标记清除(mark-and-sweep)

mark,从 root 开始进行树遍历,每个访问的对象标注为「使用中」

sweep,扫描整个内存区域,对于标注为「使用中」的对象去掉该标志,对于没有该标注的对象直接回收掉

  • 每次启动垃圾回收都会暂停当前所有的正常代码执行,回收是系统响应能力大大降低。

  • 标记需要扫描整个heap,清除数据会产生heap碎片

1.1.2 升级版三色标记

1 起初所有对象都是白色。

2 从根出发扫描所有可达对象,标记为灰色,放入待处理队列。

3 从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。

4 重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。

普通标记只有黑色和白色,没有中间的状态,这就要求GC扫描过程必须一次性完成,得到最后的黑色和白色对象,这种方式会存在较大的暂停时间。

三色标记增加了中间状态灰色,增量式GC运行过程中,应用线程的运行可能改变了对象引用树,只要让黑色对象直接引用白色对象,GC就可以增量式的运行,减少停顿时间。

1.1.3 复制收集 (Copying Garbage Collection)

节点复制也是基于追踪的算法。其将整个堆等分为两个半区(semi-space),一个包含现有数据,另一个包含已被废弃的数据。

节点复制式垃圾收集从切换(flip)两个半区的角色开始,然后收集器在老的半区,也就是 From space 中遍历存活的数据结构,在第一次访问某个单元时把它复制到新半区,也就是 To space 中去。

在 From space 中所有存活单元都被访问过之后,收集器在 Tospace 中建立一个存活数据结构的副本,用户程序可以重新开始运行了。

优点

  • 所有存活的数据结构都缩并地排列在 To space 的底部,这样就不会存在内存碎片的问题。

  • 获取新内存可以简单地通过递增自由空间指针来实现。

缺点

  • 内存得不到充分利用,总有一半的内存空间处于浪费状态。
1.1.4 分代收集(Generational Garbage Collection)

基于追踪的垃圾回收算法(标记-清扫、节点复制)一个主要问题是在生命周期较长的对象上浪费时间(长生命周期的对象是不需要频繁扫描的)。同时内存分配存在这么一个事实 “most object die young”。

基于这两点,分代垃圾回收算法将对象按生命周期长短存放到堆上的两个(或者更多)区域,这些区域就是分代(generation)。对于新生代的区域的垃圾回收频率要明显高于老年代区域。

分配对象的时候从新生代里面分配,如果后面发现对象的生命周期较长,则将其移到老年代,这个过程叫做 promote。随着不断 promote,最后新生代的大小在整个堆的占用比例不会特别大。收集的时候集中主要精力在新生代就会相对来说效率更高,STW 时间也会更短。

优点

  • 性能更优。

缺点

  • 实现复杂。

1.2 引用计数(Reference counting)

引用计数类 GC 会记录每个对象的引用次数,当引用次数为0时,就会被回收,这类 GC 实现起来较为简单。采用这类 GC 的主流语言有:Python/PHP/Perl/TCL/Objective-C/C++ 的 share_ptr

优点

  • 算法易于实现。

  • 内存单元能够很快被回收。相比于其他垃圾回收算法,堆被耗尽或者达到某个阈值才会进行垃圾回收。

  • 渐进式。内存管理与用户程序的执行交织在一起,将 GC 的代价分散到整个程序。不像标记-清扫算法需要 STW (Stop The World,GC 的时候挂起用户程序)。

缺点

  • 原始的引用计数不能处理循环引用。

  • 维护引用计数降低运行效率。

「追踪」与「引用计数」这两类 GC 各有千秋,真正的工业级实现一般是这两者的结合,不同的语言有所偏重而已。

2. GO的GC

go语言垃圾回收总体采用的是经典的 mark and sweep 算法。

2.1 并行

Go如何减短这个过程呢?标记-清除(mark and sweep)算法包含两部分逻辑:标记和清除。 我们知道Golang三色标记法中最后只剩下的黑白两种对象,黑色对象是程序恢复后接着使用的对象,如果不碰触黑色对象,只清除白色的对象,肯定不会影响程序逻辑。所以:清除操作和用户逻辑可以并发。

2.2 为什么golang的gc不整理、不分代?

对于这个问题,首先我不得不说的是,分代确实能很好的提高gc的效率,因为大多数对象使用的时间是很短的,而长时间占用的对象是很少的,这也是java中分代的原因。而对于整理,整理的话有利于内存的管理和回收,当对象被回收之后,会出现很多的内存碎片,而整理可以很好的重新规范内存,回收那些不需要的页。

那么golang为啥不做呢?首先是复杂,我们看java分代回收的实现就非常的复杂,实现起来需要很大的力气,而当前的golang的gc效率已经可能已经满足需求了。然是就是整理,其实整理这块是由内存管理模块来管理的,而golang中的内存管理在分配的阶段已经利用了最小化的原则,每次给到的都是合适的大小,所以整理这块就交由他们进行来管了,gc这块只负责回收就可以了。

2.3 何时触发 GC

sysmon()
3. 参考资料