前言
昨天提到3个术语:并行,stop-the-world,并发。当然一个垃圾收集器可以用以上3个术语中的任意一个或多个组合来描述。举个例子:一个并行并发收集器是多线程的(并行部分)同时还可以与应用同一时间执行(并发部分)
正文
并行垃圾收集器
一种并行stop-the-world收集器,简单来说就是每发生一次垃圾收集,它会停掉所有应用的线程并用多个线程执行垃圾回收工作。
垃圾回收工作可以不受任何中断,非常高效地完成,是最小化垃圾收集工作开销时间的最好方式。注:有可能会因为垃圾回收导致应用中断过长,谨慎!!!
关于年轻代和老年代在并行垃圾收集器中,回收当然都是并行的,而且会stop-the-world。但老年代的回收还会进行压缩(将邻近的对象移动到一起)消除它们之间被浪费的空间,形成一个最理想的堆布局。有个重点压缩时间长短,所以需要注意当前Java堆的大小以及老年代中存活对象的数量和大小。
在并行垃圾收集器被引入 HotSpot 的时候,只有年轻代会使用并行 stop-the-world 收集器。老年代的回收是使用一个单线程的stop-the-world 收集器。
HotSpot命令行选项是(-XX:+UseParallelGC)
在并行垃圾收集器被引入时,主要是为了应对服务端要求吞吐量最优化的使用场景,因此并行垃圾收集器成为了 HotSpot服务端虚拟机的缺省收集器。但是随着绝大多数Java堆的尺寸已经趋向于 512MB~2GB,这使得并行垃圾收集器的中断时间保持一个相对低的水平,哪怕是单线程的 stop-the-world 收集器。
好吧!随着 Java 堆的尺寸以及老年代中存活对象的数量和大小的增长,老年代的垃圾收集时间变得越来越长。与此同时,硬件的发展进步使得我们有更多可用的硬件线程。于是,通过增加一个多线程的老年代收集器与多线程的年轻代收集器同时使用的方式,并行垃圾收集器得到了增强。这使并行垃圾收集器降低了收集和压缩堆的时间开销。
增强的并行垃圾收集器随着Java6更新发布一同交付。
新的命令行选项 (-XX:+UseParalle10ldGc)来激活。
使用场景
1.对应用吞吐量的要求远远高于对延迟的要求。
批处理的应用就是一个很好的例子,因为它是非交互性质的。当你启动一个批量执行,你会希望它越快执行完越好。
2.如果能满足应用的最差延迟要求,并行垃圾收集器将提供最佳吞吐量。
最差延迟要求包含2个方面:最差延迟时间和中断发生的频度。
一个应用可能会有这样的延迟需求“超过 500ms的暂停每2小时不能多于1次,同时所有的暂停不能超过 3s”
一个交互式应用拥有足够的小块活动数据,因此并行垃圾收集器的一个full GC事件就能满足甚至超额达成对应用垃圾收集导致的最差延迟要求。
对于能满足这些要求的应用来说,并行垃圾收集器可以工作得很好。但对于那些不满足要求的应用来说,暂停时间会变得很长,因为一次full GC必须标记整个Java 堆空间,同时还要压缩老年代空间。导致的一个结果就是,随着Java 堆空间的增大,暂停时间也会随之增加。
串行垃圾收集器
串行垃圾收集器与并行垃圾收集器很相似,只是它是用一个单线程做所有工作。这种单线程的方式意味着垃圾收集器实现的复杂度更低,以及需要非常少的外部运行时数据结构,其内存占用空间大小(footprint)也是所有HotSpot 垃圾收集器里最低的。
当然,串行垃圾收集器面对的挑战与并行收集器也非常相似,中断时间可能很长,同时随着堆的大小以及活跃数据的数量变化,中断时间会呈线性增加或减少。另外,串行垃圾收集器引发的长暂停会更加明显,因为所有垃圾收集工作都是在一个线程里完成。
因为很少占用内存,在Java HotSpot客户端虚拟机中默认使用串行垃圾收集器,同时它还被用于大量嵌人式场景的需求。
HotSpot命令行选项(-XX:+UseserialGc)
明确指定了使用串行垃圾收集器来做垃圾收集。
并发标记清除CMS垃圾收集器
比串行或并行垃圾收集器有更短的中断时间,哪怕牺牲一些应用的吞吐量来消除或极大地减少漫长的GC中断数量也是能够接受的。针对这种情况,CMS垃圾收集器被开发出来。
在CMS垃圾收集器中,年轻代的垃圾收集与并行垃圾收集器很类似,它们是并行的而且会stop-the-world,也就是说在年轻代的垃圾收集过程中所有的Java应用线程都会被暂停,而垃圾收集工作会用多线程的方式来执行。
你可以给CMS垃圾收集器配置一个单线程模式的年轻代收集器,但在Java8中并不推荐这个方式,这个选项在Java9中被移除了。
并行垃圾收集器与CMS垃圾收集器最主要的区别是在老年代的收集上。CMS收集器 的老年代收集活动试图避免应用线程的长时间中断。为了实现这个目的,CMS老年代收集 器在应用线程执行的同时做了大部分工作(垃圾收集线程与应用线程同时工作)除了少量相对短的 GC同步暂停。
通常来说,绝大多数情况下CMS是并发的,老年代收集的某些阶段会暂停应用线程,比如初始标记和重新标记阶段。在CMS最初的实现中,初始标记和重新标记阶段都是单线程的,但现在它们都已经被改为多线程的。激活多线程的初始标记和重新标记阶段的 HotSpot 命令行选项分别是:
-XX:+CMSParallelInitial Mark Enabled
-XX:CMSParallelRemarkEnabled
注意:通过命令行选项XX:+UseConcurrentMark SweepGC激活CMS垃圾收集器时也会缺省自动激活这两个选项。
使用CMS垃圾收集器面临的一个挑战就是要在应用消耗完Java的可用堆空间之前完成并发收集工作。因此对CMS来说有个很棘手的部分,就是找到一个合适的时机来启动这个并发工作。这种并发方式往往导致一个结果,就是处理同一个应用,CMS GC会比并行GC多占用10%~20%的Java堆空间。这也是为了缩短垃圾收集暂停时间所付出的代价。
CMS垃圾收集器的另一个挑战是如何处理老年代中的空间碎片,也就是当老年代中对象间的空间碎片太小,以至于无法容纳从年轻代晋升上来的对象,因为在CMS的并发收集循环中并不执行压缩,哪怕是增量或局部压缩。一旦无法找到可用空间,就会使CMS 回过来使用串行 GC,触发一次 full 收集,导致一个漫长的暂停。伴随 CMS 碎片的另一个很不幸的挑战就是上述问题完全无法预测。同样都是老年代碎片,某些应用可能没有经历过一次full GC,而有些可能时不时就要经历一次。
对CMS垃圾收集器做些调整,对应用做些优化改动,诸如避免生成大尺寸对象,会有助于延缓空间碎片的产生。当然,调优本就是个不寻常的工作,对专业能力有很高的要求, 所以改动应用来避免碎片也是个不小的挑战。
总结
好吧!到目前为止,以上提到的垃圾收集器都有几个共同的问题。一个就是老年代收集器的大部分操作都必须扫描整个老年代空间,比如标记、清除,以及压缩。这意味着执行工作的时间将随着 Java堆空间的变化而线性地增加或减少。
另一个问题就是因为年轻代和老年代是独立的连续内存块,所以要先决定年轻代和年老代放在虚拟地址空间的什么位置。