关于 Go 语言最新的垃圾回收器(garbage collector),我最近阅读了许多篇赞赏它的文章,可是它们都让我将信将疑,其中的很多来自 Go 语言的官方团队博客。他们像是暗示着在垃圾回收领域已经发生了一个巨大的突破。算法
如下是这个垃圾回收器在 2015 年 8 月第一次被公之于众时的摘录:编程
Go 正在准备构建一个不只属于 2015 年更属于 2025 年及将来的垃圾回收器。Go 1.5 的垃圾回收将会预示着 stop-the-world 再也不会成为构建一个安全的编程语言的壁垒。届时应用能够被轻松高效的在硬件之间扩展。而且随着硬件愈来愈强大,软件的扩展性也会变得愈来愈强大,垃圾回收再也不会成为其中的障碍。缓存
Go 团队不只声称他们已经解决了垃圾回收中的 stop-the-world 问题,而且还表示这将使你的编程体验会愈加简易:安全
目前一个比较高层次抽象且解决垃圾回收性能问题的方案是添加更多的垃圾回收预配置。编程人员能够根据他们应用程序的具体状况,选择不一样的预配置来启动应用。这个方案的缺点就是,随着时间的推移,预配置变得愈来愈多,你也渐渐进入了其中的选择综合症中不能自拔。Go 的解决方案彻底与之相反,它仅仅提供一种预配置,即 GOGC 。服务器
看到这些有关新运行时的消息,Go 语言的开发者们无疑都很是开心。可是这些话仅仅都只是博客世界中的摘录,让咱们先冷静下来,仔细深刻推敲它们。架构
现实是 Go 的新垃圾回收器并无真正地使用任何新的概念或新的研究成果。Go 团队在声明中也认可,新的垃圾回收器中的并发 标记/删除 模型早在 1978 年就已被提出。这个新的垃圾回收器之因此吸引眼球,仅仅是由于它被设计来最小化垃圾回收中的暂停时间,但其实它付出了垃圾回收中其余全部重要方面的妥协代价。Go 团队彷佛并无告诉市场这些权衡所付出的代价。因此,咱们能够将事实归纳为:并发
咱们使用了一个 10 多年前的算法,创造了一个属于将来 10 年的垃圾回收器。Go 的新垃圾回收器是一个并发的,三色的,标记/删除 回收器。该算法最先由 Dijkstra 在 1978 年提出。它与目前几乎全部的“企业级”垃圾回收解决方案都不一样,仅单独使用它这一种方案,便可很好的适应现代硬件,以及符合现代软件所要求的低延迟。编程语言
因此,这 40 年来,在“企业级”垃圾器回收器领域中的研究,都是一无所得么?仍是...工具
当设计一个垃圾回收算法时,如下是你须要考虑的众多不一样因素:性能
程序吞吐量: 你的算法会拖慢程序多久?它一般使用有多少百分比的 CPU 时间被用于 垃圾回收 vs 真正的工做 来衡量。
垃圾回收吞吐量: 在恒定的 CPU 时间内,回收器能够清理多少垃圾?
堆使用量: 你的垃圾回收器还需使用多少额外的堆内存?
暂停时间: 你的垃圾回收器一次会 stop-the-world 多久?
暂停频率: 你的垃圾回收器多久会 stop-the-world 一次?
暂停分布: 你的垃圾回收器会时而暂停好久而又时而短暂暂停么?或者仍是比较恒定?
内存分配效率: 分配新内存是高效的,低效的,仍是不可预测的?
内存紧凑度: 你的垃圾回收器会由于有较多内存碎片,而在还有足够内存的状况下,在申请内存时抛出内存不足错误吗?
并发性: 你的垃圾回收器是否能很好地使用多核处理器?
扩展性: 在堆变得愈来愈大时,你的垃圾回收器仍能很好的工做吗?
可配置性: 你的垃圾回收器的配置会很复杂吗?
热身时间: 你的垃圾回收器是否会根据环境状况自我调整?若是是,它须要多长时间来达到最佳状态?
内存释放: 你的算法是否会将再也不使用的内存释放回操做系统中?若是是,在什么时候?
可移植性: 你的垃圾回收器是否能在不一样的 CPU 架构中工做?
兼容性: 你的垃圾回收器为哪一个编程语言和编译器服务?它是否可为那些无需垃圾回收的语言(如 C++)服务?当改变垃圾回收算法时,是否须要从新编译整个程序以及依赖?
正如你所见,在设计一个垃圾回收器时,有许许多多须要考虑的因素。其中的一些甚至会影响围绕该平台的生态。
正由于要考虑的因素如此多且复杂,垃圾回收已是计算机科学研究中的一个子领域。新的算法不断地被提出,而后在学术界和工程界中被实现。可是不幸的是,尚未单独一个算法,能够完美适用于全部状况。
让咱们说的更具体一些。
第一个垃圾回收算法是为单处理器机器中使用小规模堆内存的程序而设计的。CPU 和内存很是昂贵,而且用户对程序的性能没有特别高的要求,因此肉眼可以看见的暂停也是容许的。那时的算法设计主要以最小化使用 CPU 时间和堆内存容量来设计。这觉得在你没法继续分配内存以前,垃圾回收都不会启动。当没法分配之时,垃圾回收器将会暂停程序,而后在堆中执行一个全量的 标记/删除 回收。
这类的垃圾回收器十分古老,但它们仍有一些可见的优势:它们的实现十分简单,在不回收时它们不会拖慢你的程序,而且不会占用额外的内存。在一些保守的回收器(如 Boehm )中,它们甚至不须要随着你的编译器和编程语言而改变!这使得它们很是适合于使用小量堆内存的桌面程序,例如 AAA 视频游戏。
让咱们转向另外一种状况,若是你正有一个 10 核处理器并使用着几百 GB 的堆内存。或许你的服务器正在处理金融市场中的交易,或者正在运行一个搜索引擎,所以暂停时间的短暂对你很是重要。在这样的状况下,你可能须要一个在后台进行的低暂停时间的但会下降程序速度的算法。
因此说不存在单独一个算法完美适合全部的状况。也没有一个编程语言运行时能够知道你正在执行的是一个批量操做仍是一个延迟敏感的交互程序。这就是为何“垃圾回收预配置”开始出现。并非由于运行时工程师们愚钝,而是由于咱们在计算机科学领域目前的能力所限。
自从 1984 年后,人们开始了解到,大多数分配的内存在被分配后的很短期内,就已经能够被回收。这个观察被称为分代假设,而且是整个编程语言界最强大的经验发现之一。即便经历了这十多年来工程界和编程语言的变化,它依然被证明是十分正确的。
这个发现对于垃圾回收算法而言意味深长,这意味着算法能够利用它。新的分代垃圾回收器相比旧的纯 标记/删除 类型的回收器有如下改进:
垃圾回收吞吐量: 它能够以更快的速度回收更多的垃圾。
内存分配效率: 分配内存再也不须要搜索整个堆来寻找空处,因此分配内存变得十分高效。
程序吞吐量: 分配的内存被放置得十分整齐紧凑,这优化了缓存的使用。虽然分代回收器会让程序在运行时执行一些额外的工做,可是因为其优化了缓存,这使得结果利大于弊。
暂停时间: 大多数(并不是所有)的暂停时间变得更短了。
固然,它也产生了如下缺点:
兼容性: 实现一个分代垃圾回收器须要有移动内存中实体的能力,而且须要在程序向指针写入时作一个额外的工做。这意味着回收器必须和编译器紧密集成。因此如今并无为 C++ 而做的分代垃圾回收器。
堆使用量: 分代垃圾回收器须要再多个“空间(spaces)”中来回分配和复制。所以,这增长了堆的使用量。
暂停分布: 因此此时许多的垃圾回收暂停已是很是短暂的了,但有时仍须要对全堆进行较慢速的 标记/删除 。
可配置性: 分代回收器发明了“新生代”和“老生代”的概念,这使得程序的性能对于“生代”的具体大小变得敏感。
热身时间: 为了缓解可配置性问题,一些分代回收器动态地根据程序的运行状况调整“新生代”的大小。可是这样一来,暂停时间就会随着程序的运行时间而改变。
虽然有以上这些缺点,可是因为瑕不掩瑜,因此几乎全部的现代垃圾回收算法都是分代的。分代垃圾回收算法也能够与许多其余的特性相结合,如并发,并行,紧凑内存等等。
因为 Go 是一个具备类型系统且相对普通的命令式语言,它的内存访问模式能够与 C# 相比较。因此它的运行时使用的回收器与 .NET 相似,都是分代的。
事实上大多数 Go 程序也是有着像 HTTP 服务器那样的 请求/响应 模式,这意味着 Go 程序会展示出强烈的分代趋势。因此 Go 团队也正在探索将来的一个“面向请求的回收器”。但这个回收器也已被观察仅是一个具备两种可调节策略的分代回收器。在任何其余的 请求/响应 模型的运行时里,这个垃圾回收器均可以被模仿出来,只需保证“新生代”空间足够大,能够撑满全部的请求数据便可。
除此以外,Go 如今的垃圾回收器并非分代的。其他的部分就是古老的 标记/删除 回收器。
这么作你能够获得一个好处,那就是你能够拥有很是很是低的暂停时间。可是,在几乎其余全部的方面,你都要付出代价:
垃圾回收吞吐量: 随着堆的增加,垃圾回收所需的时间也随之增长。即当你的程序使用愈来愈多的内存的时候,你的内存会被释放得愈来愈慢,垃圾回收 vs 实际工做的比例也变得愈来愈高。惟一可让以上说话做废的可能就是,让你的程序彻底不并行,而且让垃圾回收同时在其余核中没有限制地运转。
内存紧凑度: 因为有大容量的“新生代”空间,因此内存彻底不紧凑。
程序吞吐量: 因为每一个循环垃圾回收都有许多的事情必需要作,必然致使它将使用更多的 CPU 时间。
暂停分布: 任何的并发垃圾回收器都会遇到一个在 Java 世界中被称为“并发模式失败”的状况:你的业务线程制造垃圾的速度比回收器线程清理的速度更快。在这个状况下,回收器只能彻底中止你的业务线程,来等待清理完全完毕。所以,虽然 Go 团队声称他们的垃圾回收暂停很是短暂,但这也仅在回收器有足够 CPU 时间来保证跑得比业务程序快的状况下才是真实的。除此以外,Go 的编译器自身还缺乏可以可靠地当即暂停业务程序的特性。因此,暂停时间是否真的短暂,取决于你正在跑什么样的业务代码(例如使用 base64 解压大块数据可使暂停时间快速增涨)。
堆使用量: 由于使用 标记/删除 清理堆会很是低效缓慢。因此 Go 须要大容量的“新生代”空闲空间来保证你不会遇到“并发模式失败”。因此,Go 默认会让你的堆使用量多出 100 %...
因此,Go 对于暂停时间的优化,代价几乎是让其他部分的代码都变得更慢了。
HotSpot JVM 提供了许多垃圾回收算法,你能够在命令行里选择其中之一。这些算法中并无一个的目标是为了获得像 Go 同样的超短暂暂停时间,由于它们都还有考虑其余方面的妥协因素。用户能够经过重启程序来切换垃圾回收算法。因此当用户为了避免同的场景进行代码调优时,能够尝试不一样的算法。
在任何的现代化计算机上,默认的算法都是高吞吐量回收器。它为执行大批量任务而设计,而且并无在暂停时间方面有所特别优化。虽然这个默认选项让人们会认为 Java 的垃圾回收作的并很差,可是在黑盒以外,Java 仅仅是试图让你的程序能够跑到最快速,而且使用最少的内存,尽管暂停时间可能不乐观。
若是更多的暂停时间对你来讲很是重要,那么你能够切换至并发 标记/回收 回收器(CMS)。这是与 Go 的垃圾回收器最接近的一个。它也是分代的,但它会有比 Go 的垃圾回收器更长的暂停时间:“新生代”空间在暂停时,除了回收垃圾外,还会试图经过移动对象来让本身变得更紧凑。在 CMS 中有两种暂停。第一种是较快速的,它大约须要 2-5 毫秒。第二种是慢速的,大约须要 20 毫秒。CMS 也是自适应的:由于它是并发的,因此它必须去猜想合适开始执行(正如 Go 同样)。然而 Go 要求预先使用大量额外的堆内存在避免“并发模式失败”,但 CMS 会在运行时进行自适应来尝试避免。
最新一代的 Java 垃圾回收器被称做 “G1” ,意为 “垃圾回收优先(garbage first)”。虽然它并非 Java 8 的默认选项,但会是 Java 9 的。它被设计用来做为一个当下最通用普适的算法。对于几乎整个堆,都是并发,分代且内存紧凑的。它也能够根据环境进行自适应,可是正如全部的回收算法同样,它并不知你程序的真正意图,因此它也容许你执行一些额外的可配置参数:如它可使用的最大内存量,以及目标暂停时间,而后会它调整其余的一切来尽力知足你的要求。G1 默认更倾向于让你的程序跑的更快,而不是拥有更短的暂停时间,因此默认的目标暂停时间是 100 毫秒。每次的暂停时间也并非恒定的,大多暂停都很是短暂。大多数在使用了 TB 级别的堆内存的环境下,G1 的表现也很是不错。故 G1 的扩展性也很是不错。
最后,还有一种名为 Shenandoah 的新垃圾回收算法。它已进入 OpenJDK ,但不会再 Java 9 中出现。除非你使用来自 Red Hat 的特殊 Java 构建版本。它被设计为在任意的堆大小下,都拥有很是短暂的暂停时间,而且还能够保持内存紧凑。代价则是更多的堆使用量和实现复杂度。它须要在应用仍在运行时就能够移动对象的位置,这就要求指针地址的读和写操做都要和垃圾回收器进行交互。
这篇文章的目的并非说服去使用另外一门编程语言或者工具。而是仅仅想要表达:垃圾回收是一个异常复杂的问题。因此对于这一领域中一切所生成的突破性成果都须要先保持一个怀疑的态度。它们颇有可能仅仅是没有说出其余方面的权衡而已。
可是,若是你并不介意其余方面的代价,而仅仅想要最小化暂停时间,那么,请使用 Go 的垃圾回收器。