golang 垃圾回收机制
本文内容纲要:
-常见GC算法
-1.引用计数法
-2.Mark-Sweep法(标记清除法)
-3.三色标记法
-4.分代收集
-golang的GC
用任何带GC的语言最后都要直面GC问题。在以前学习C#的时候就被迫读了一大堆.NETGarbageCollection的文档。最近也学习了一番golang的垃圾回收机制,在这里记录一下。
常见GC算法
趁着这个机会我总结了一下常见的GC算法。分别是:引用计数法、Mark-Sweep法、三色标记法、分代收集法。
1.引用计数法
原理是在每个对象内部维护一个整数值,叫做这个对象的引用计数,当对象被引用时引用计数加一,当对象不被引用时引用计数减一。当引用计数为0时,自动销毁对象。
目前引用计数法主要用在c++标准库的std::shared_ptr、微软的COM、Objective-C和PHP中。
但是引用计数法有个缺陷就是不能解决循环引用的问题。循环引用是指对象A和对象B互相持有对方的引用。这样两个对象的引用计数都不是0,因此永远不能被收集。
另外的缺陷是,每次对象的赋值都要将引用计数加一,增加了消耗。
2.Mark-Sweep法(标记清除法)
这个算法分为两步,标记和清除。
- 标记:从程序的根节点开始,递归地遍历所有对象,将能遍历到的对象打上标记。
- 清除:讲所有未标记的的对象当作垃圾销毁。
图片来自https://en.wikipedia.org/wiki/Tracing_garbage_collection
如图所示。
但是这个算法也有一个缺陷,就是人们常常说的STW问题(StopTheWorld)。因为算法在标记时必须暂停整个程序,否则其他线程的代码可能会改变对象状态,从而可能把不应该回收的对象当做垃圾收集掉。
当程序中的对象逐渐增多时,递归遍历整个对象树会消耗很多的时间,在大型程序中这个时间可能会是毫秒级别的。让所有的用户等待几百毫秒的GC时间这是不能容忍的。
golang1.5以前使用的这个算法。
3.三色标记法
三色标记法是传统Mark-Sweep的一个改进,它是一个并发的GC算法。
原理如下,
- 首先创建三个集合:白、灰、黑。
- 将所有对象放入白色集合中。
- 然后从根节点开始遍历所有对象(注意这里并不递归遍历),把遍历到的对象从白色集合放入灰色集合。
- 之后遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合
- 重复4直到灰色中无任何对象
- 通过write-barrier检测对象有变化,重复以上操作
- 收集所有白色对象(垃圾)
图片来自https://en.wikipedia.org/wiki/Tracing_garbage_collection
过程如上图所示。
这个算法可以实现"on-the-fly",也就是在程序执行的同时进行收集,并不需要暂停整个程序。
但是也会有一个缺陷,可能程序中的垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉。
使用这种算法的是Go1.5、Go1.6。
4.分代收集
分代收集也是传统Mark-Sweep的一个改进。这个算法是基于一个经验:绝大多数对象的生命周期都很短。所以按照对象的生命周期长短来进行分代。
一般GC都会分三代,在java中称之为新生代(YoungGeneration)、年老代(TenuredGeneration)和永久代(PermanentGeneration);在.NET中称之为第0代、第1代和第2代。
原理如下:
- 新对象放入第0代
- 当内存用量超过一个较小的阈值时,触发0代收集
- 第0代幸存的对象(未被收集)放入第1代
- 只有当内存用量超过一个较高的阈值时,才会触发1代收集
- 2代同理
因为0代中的对象十分少,所以每次收集时遍历都会非常快(比1代收集快几个数量级)。只有内存消耗过于大的时候才会触发较慢的1代和2代收集。
因此,分代收集是目前比较好的垃圾回收方式。使用的语言(平台)有jvm、.NET。
golang的GC
go语言在1.3以前,使用的是比较蠢的传统Mark-Sweep算法。
1.3版本进行了一下改进,把Sweep改为了并行操作。
1.5版本进行了较大改进,使用了三色标记算法。go1.5在源码中的解释是“非分代的、非移动的、并发的、三色的标记清除垃圾收集器”
go除了标准的三色收集以外,还有一个辅助回收功能,防止垃圾产生过快手机不过来的情况。这部分代码在runtime.gcAssistAlloc
中。
但是golang并没有分代收集,所以对于巨量的小对象还是很苦手的,会导致整个mark过程十分长,在某些极端情况下,甚至会导致GC线程占据50%以上的CPU。
因此,当程序由于高并发等原因造成大量小对象的gc问题时,最好可以使用sync.Pool
等对象池技术,避免大量小对象加大GC压力。
go采用三色标记和写屏障:
- 起初所有的对象都是白色
- 扫描找出所有可达对象,标记为灰色,放入待处理队列
- 从队列提取灰色对象,将其引用对象标记为灰色放入队列
- 写屏障监视对象的内存修改,重新标色或放回队列
关于go的写屏障(writebarrier),可以阅读最近一篇比较热的文章《Proposal:EliminateSTWstackre-scanning》。作者主要介绍下个版本Go为了消除STW所做的一些改进,包括写屏障的优化方式。
并发的三色标记算法是一个经典算法,通过writebarrier,维护”黑色对象不能引用白色对象”这条约束,就可以保证程序的正确性。Go1.5会在标记阶段开启writebarrier。在这个阶段里,如果用户代码想要执行操作,修改一个黑色对象去引用白色对象,则writebarrier代码直接将该白色对象置为灰色。去读源代码实现的时候,有一个很小的细节:原版的算法中只是黑色引用白色则需要将白色标记,而Go1.5实现中是不管黑色/灰色/白色对象,只要引用了白色对象,就将这个白色对象标记。这么做的原因是,Go的标记位图跟对象本身的内存是在不同的地方,无法原子性地进行修改,而采用一些线程同步的实现代价又较高,所以这里的算法做过一些变种的处理。
本文内容总结:常见GC算法,1.引用计数法,2.Mark-Sweep法(标记清除法),3.三色标记法,4.分代收集,golang的GC,
原文链接:https://www.cnblogs.com/diegodu/p/9150840.html