GC 是一种自动管理内存的技术,用来回收(释放) heap 中不再使用的对象。
GC 过程中涉及到两个阶段:
区分活对象(live object)与垃圾对象(garbage)
回收垃圾对象的内存,使得程序可以重复使用这些内存
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的GCgo语言垃圾回收总体采用的是经典的 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. 参考资料