现代的垃圾回收机制(Go 垃圾回收机制概述)

2019-03-07  本文已影响33人  雨生_

现代的垃圾回收机制(Go 垃圾回收机制概述)

关于 Go GC策略的见解

细节你可以到 Hacker NewsReddit 查看相关内容

最近我看到了很多关于Go 最近的垃圾回收机制的推广文章,甚至有一些来自于Go项目组,从他们的文字中感受到,Go的垃圾回收机制似乎发生了根本性的突破。
这里有一篇2015年8月份的,关于最新的收集器的介绍

Go 语言构建了一个新的GC机制,不只为了应对2015,甚至2025年以及更远的时间。Go 1.5版本的GC 迎来了一个全新的改变,让STW(Stop-the-world)的暂停不再是语言转向安全可靠的语言的障碍。在未来,应用程序随着硬件一起扩展变得更容易,GC不再是阻碍硬件变得更有强大的障碍,这是未来十年甚至更长时间上的一个突破。

同时Go的团队说,新的GC不仅解决了暂停的问题,而且让这个事情变得更简单。
此外,不受外界的影响,我们的进行时团队可以专注于提高用户程序反馈的真实问题。

毫无疑问,Go的很多用户对于这个新的运行时机制非常的满意,但是我对于这种说法还是有一些疑虑 —— 对我来说,这种说法就像是一种虚幻(misleading)的推销方式。由于这种说法在博客圈内反复出现,我们应该对于这件事进行深入的关注了。
事实上,Go GC并没有实现更多的新创意和新的研究成果。正如他们所公示的内容,他是一个很直接的并发标记/扫描收集的机制,这种方式还是基于1970年的研究想法。GC唯一不同的是他通过牺牲一些其他合理的性能来优化暂停的时间,但是GC的技术发言和推销材料上,却没有提及他们牺牲了什么内容,让那些不熟悉或者不关心垃圾回收机制的人忘记了这段牺牲的内容,甚至暗示出,Go语言的其他竞争对手(Java等)的垃圾回收很差。Go鼓励以下这种看法:

为了创造一个应用于未来10年的垃圾回收器,我们研究了过去几十年的算法内容。Go的新垃圾回收机制,是采用并发的三色标记法收集器,这个想法在1987年被Dijkstra提出。这个“企业级”垃圾回收期在今天依然是一个争议很大的产品,但我们坚持认为,对于当今时代的硬件设备,他是一个最适合的并且符合现代计算机对于延迟要求的产物。

读完了上述的内容,你可能会觉得过去40年的企业级GC架构根本没有任何价值。

GC相关的理论读物

关于设计一个GC的垃圾回收器,我们需要考虑一下方面:

正如你所看到的,设计垃圾收集器设计很多因素,其中一些因素甚至会影响你平台下的一些生态的内容。而且我也不确定我以上列举的内容是否有遗漏,可能还有更多未曾提及的内容。

由于设计的复杂性,垃圾回收期一直是众多计算机科学领域研究论文的一个子领域。学术界和工业界都在研究并实现 稳定的速度 的新算法。但是不幸的是,没有人研究出能够适合所有场景的算法。

权衡无处不在

让我们更具体的来聊聊

第一代垃圾回收算法呗设计用于单处理器,很小堆的程序上面。那时候CPU和RAM非常贵,而且用户要求的不高。所以肉眼可见的暂停也可以让人接受。所以这个时代的算法,优先考虑最小化的堆内存和CPU开销。这意味着,在你没有分配新内存的时候,GC什么也不做,在需要的时候,他会启动,然后程序暂停,并且他需要完成所有的堆标记,扫描,尽可能快的释放一些不需要的局部区域。

尽管这样的垃圾收集器很旧,但是仍然有一定的学习价值。他非常简单,在不需要收集的时候,他不会拖慢你的程序,也不需要添加多余的堆内存。对于像Boehm GC这样的保守派的收集器,他们甚至不需要改变编辑器和编程语言。这可以使它们适用于通常具有小堆的桌面应用程序,包括AAA视频游戏,其中大部分RAM由不需要扫描的数据文件占用。

Stop-the-world (STW) 标记/扫描 是现在垃圾回收机制最常用的GC算法,在进行面试的时候,我经常会让候选人谈谈GC,但是不幸的是,候选人一般都把GC当做一个黑盒,几乎对他没有任何了解,有的甚至认为他是一个非常老的技术。

问题是简单的标记扫描算法,性能很差。随着你的核数和内存变大,这个算法甚至会停止工作。但是,通常情况下,通过更小的堆划分,来控制暂停的时间也是足够了。这种情况下,你可能更希望使用这种方式来保证低消耗。

另一种情况是,你可能用的是数十核 加上数百GB的机器,你的服务可能在进行复杂的金融交易或者执行一个搜索引擎,这种服务上面,低停顿对你来说可能很重要,在这些情况下,你可能更希望使用一种算法,在运行时降低程序的速度,以便于回收器在后台进行短暂停顿的收集。

这不是一个简单的系列,在更大的后端里面,可能有大批量的工作,非交互式的暂停无关紧要,只在意总的运行时间。这种情况下,最好使用一个比较大吞吐量的算法来覆盖他们呢。即完成有用的工作与收集时间的比例问题。

以上的三种情况,告诉了我们,没有一个单一的垃圾回收算法能同时适用于所有的项目,没有一个编程语言能知道,你的程序是一个有很多计算任务的服务,或者是一个对延迟很敏感的程序。这就是GC 调优存在的原因,这并不是因为工程师愚蠢,侧面反映了计算机科学能力上的一些限制。

分代设计

在1984年,这个想法就被大家所熟知,就是收集器分配的 "年轻代",即在分配内存后很快被回收的内容。这种想法只是一种假设上的划分,也算是整个PL工程领域中最强大的划分之一。他在不同的编程语言和软件行业数十年的变革中,始终如一。在很多函数式语言,命令式语言,弱类型语言中,也是这样做的。

理解这个东西对于程序很有用,因为GC算法的设计或多或少都会参照这个思路。一些新的垃圾回收机制,在旧的 停止-标记-扫描 的风格上,都有很多的改进。

同样的也带来了一些负面的效果

然而整体上来说,好处是巨大的,基本上所有的现代的GC算法都是基于这个基础上做的,分代回收器可以通过各种方式增强功能,典型的现代的GC算法,基本上都同时具备平行,并行,生成,压缩的能力。

Go并发收集器

Go语言属于一种普通的拥有值类型的解释性语言,因此内存访问模式与C#相比,Go的当代假设成立,而.NET使用的是分代收集方式。
事实上,go程序的request和Response更像是http服务器,go程序展现出了极强的行为,go开发团队探索面向请求的收集起的开发模式。这就意味着他对于GC问题的内核已经发生了本质的改变。GC可以为request/response 处理器确保他足够大的新生代空间,所有的垃圾·都可以通过处理并符合要求

尽管如此,Go目前的GC还不是一个多代的模型,只是一个后台普通的旧的标记扫描模型。

这样做也有一个好处,你可以获得非常低的暂停时间,但是几乎所有的其他事情都会变得很糟糕。通过我们以上的结论,不难看出:

关于上述内容的权衡问题,我们可以看这篇文字

由于Server 1 分配的内存高于 Server 2, 所以STW的暂停时间1比2 更高,但是 暂停的持续时间,相对来说还是下降了一个维度,在切换两个服务之后,CPU的使用量,增加了20%

所以在这个特殊的情况下,Go尽管暂停时间下降了,但是同样也拖慢了收集器的速度。这种方式是一个足够平衡的模式?还是说暂停时间无法继续优化了?官方没有给与一个解释。
但是这就产生了一个问题,付出更多的硬件来降低暂停时间是否有意义,如果你的暂停时间从10ms 降到1ms,用户是否可以真实感受到?是否值得你扩大两倍的硬件去得到它。
Go 优化暂停时间的方式,就是减慢你程序的执行效率,来获取更快的暂停。

与Java对比

HotSpot JVM 有众多的GC算法供你选择,没有像Go这样处理暂停时间,因为他们会权衡他们GC算法之间的区别,可以通过对比来比较谁好谁坏。只需要重启你的程序,并在GC算法中做选择,通过实践决定你用哪一个算法来执行你的程序。

任何现代的计算机,默认的收集算法都是吞吐量优先的收集器。这个是为了批量作业而设计的,默认情况下没有暂停时间的目标。之所以选择这个算法作为默认算法的原因是,所有人都开箱即用。所以Java只能让你的程序尽可能快的运行,减少内存开销和暂停时间。

如果你对于暂停时间特别敏感,那推荐你选择(CMS)的方式,这个是跟Go语言使用的最接近的算法,但是CMS同样也是基于分代策略的,这也是为什么他暂停时间比Go长的原因:
年轻代因压缩而应用暂停,因为他需要移动内存。CMS有两种暂停类型,一种是比较快的机制,大概2-5 ms,另一种大概要20ms,CMS是一种自适应的模型,因为他是并发的,他必须要像Go一样猜测执行的时机。Go建议你配置堆的开销来进行调整,而CMS是在运行时进行自适应,来防止并发模式失败。由于是普通的标记扫描方式,所以碎片化会导致减速的问题。

Java最新的垃圾回收期叫做"G1",寓意是"garbage first", 他并不是Java 8 默认的机制,在Java 9 之后会被设置为默认的机制。这种算法希望能够实现一刀切的方式。它主要是整个堆上面的并发,压缩和生成。他同样的也是自我调整的。但是就像所有的GC算法无法理解你的真实需求一样,他只能通过一些配置的方式,帮你完成一些平衡的操作。你只需要告诉他最大的RAM量和允许的最大暂停时间,他会自我调整其他的参数,来保证你的要求。默认的最大暂停时间是100ms。如果你不调整这些内容,G1的默认思路就是让你的应用运行的更快,暂停的更短。
同样的暂停的时间也不都是一致的,大多数情况下,都非常快(小于1ms),当堆被压缩的时候,会比较慢(50ms左右), G1的伸缩性很好,有报道称在TB级别的堆数据中,依然表现良好,而且还有一些比较酷的能力,例如在堆中复制字符串。

最后,一种名为Shenandoah的新GC算法。 它是OpenJDK贡献的,但除非您使用Red Hat(赞助项目)的特殊Java构建,否则不会使用Java 9。 无论堆大小如何,它都可以提供非常低的暂停时间,同时仍然可以进行压缩。 成本是额外的堆开销和更多障碍:在应用程序仍在运行时移动对象需要指针读取和写入以与GC交互。 在这个意义上,它类似于Azul的“无动作”收藏家。

结论

本文的主要观点并不是为了说服你使用不同的编程语言和工具,只为了让你理解一件事,GC是一个难题,真的非常难,几十年来一直都由众多的计算机科学家不断研究。所以
不要怀疑或者相信别人错过了某些思想上的突破,他们可能只是通过一些伪装或者不同寻常手段来平衡得失,避免一些可能带来的原因而做出努力。

如果你希望牺牲一切来达到最小的暂停时间的话,那强烈建议你,阅读GoGC

原文链接:Modern garbage collection

作者:Mike Hearn

译者:JYSDeveloper

上一篇下一篇

猜你喜欢

热点阅读