上图展示了7种,适用于不同分代中的收集器。如果两者之间由连线,说明可以搭配使用。
PS:在JDK8中将Serial+CMS和ParNew+Serial Old的组合声明为废弃,并且在JDK9中完全取消了这两种组合的支持。
1、Serial收集器
Serial收集器是JVM中最早、最基础的一个收集器。这个收集器是一个单线程工作的收集器。采用标记复制算法。它的“单线程”并不仅仅说明它是使用一个处理器或单个收集线程去完全收集工作,更重要的是强调,在它进行垃圾收集工作时,需要将其他所有工作线程(包括用户的工作线程)全部停掉,直到它收集结束(Stop The World)。
下图所示,是Serial/Serial Old收集器的运行过程:
由于Serial收集器的简单高效(与其他收集器的单线程相比)、额外内存消耗最小的特点,Serial收集器更适合用于在单核处理器或者处理器核心较小的的环境上。
2、ParNew收集器
ParNew实际上是Serial收集器的多线程的并行版本。采用的也是标记复制算法。除了使用多线程进行垃圾收集之外,其他的行为包括Serial收集器可用的参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器一致。
ParNew+Serial Old收集器的运行过程:
但是由于,在JDK9版本中不在支持ParNew+Serial Old的组合,因此只有CMS才能配合ParNew使用。
在JDK5中,HotSpot发布一个跨时代的垃圾收集器,CMS收集器。这款收集器是HotSpot虚拟机中第一个支持并发的垃圾收集器,它首次实现了让垃圾收集线程和用户线程(几乎)同时工作。
但是CMS作为,老年代的垃圾收集器,CMS却不能配合Parallel Scavenge收集器一起使用。所以在选择使用CMS收集器收集老年代时,只能选择使用Serial(JDK9之后废弃)和ParNew收集器其中的一个来收集新生代。ParNew是激活CMS后(使用-XX:+UseConcMarkSweepGC)的默认新生代收集器。也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。但是在JDK9中,官方希望新发布的G1收集器能取代ParNew+CMS的组合来作为服务端的垃圾收集方案。并且直接取消了Serial+CMS和ParNew+Serial Old的组合支持,甚至还取消了-XX:+UseParNewGC参数。这也就意味着,ParNew和CMS从此只能搭配使用。
PS:CMS不能配合Parallel Scavenge使用的原因:
1、CMS面向低延迟、Parallel Scavenge面向高吞吐量。目标不一致。
2、Parallel Scavenge和G1收集器都没在使用HotSpot中原本设计的垃圾收集器的分带框架,而选择另外独立实现。
3、Parallel Scavenge收集器
Parallel Scavenge收集器是新生代的垃圾收集器,采用的也是标记复制算法。也是能够并行收集的多线程收集器。
Parallel Scavenge收集器关注点和其他的垃圾收集器不同,CMS等垃圾收集器关注点是尽可能的缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞度量。所谓的吞吐量就是处理器用户执行用户代码的时间与处理器总耗时的比值。
如果虚拟机完成某个任务,总耗时是100min,执行用户的代码时间是99min那么吞吐量就是99%。停顿时间越短,越适合需要与用户交互或者需要保证服务响应质量的程序。
Parallel Scavenge提供两个参数用户精准控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMills参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
1、-XX:MaxGCPauseMills:允许用户设置一个大于0的毫秒数。收集器将尽量保证内存回收的时间不超过用户设定的值。但是这个值不是越短越好,垃圾收集的停顿时间是以牺牲新生代大小和吞吐量换取的。
2、-XX:GCTimeRatio:这个值是一个在0到100之间的整数。也就是垃圾收集时间占总时间的比率。相当于吞吐量的倒数。比如:将此值设置为19,那么允许最大的垃圾收集时间就占总时间的5%(即 1 / ( 1 + 19 ) 1/(1+19) 1/(1+19))。
由于和吞吐量关系密切,Parallel Scavenge收集器也经常被称为“吞吐量优先收集器”。除了上述两个参数,Parallel Scavenge还有个参数-XX:+UseAdaptiveSizePolicy。这是一个开关参数,当这个参数被激活后,就不要人工指定新生代(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大吞吐量。这种调节方式称为垃圾收集器的自适应的调节策略。
4、Serial Old收集器
Serial Old收集器是老年代版本,它同样是一个单线程的收集器。采用标记-整理算法。这个收集器和Serial收集器类似都是主要用在客户端模式下的HotSpot虚拟机中。
用在服务端模式下,可能存在两种场景:
1、在JDK5及之前的版本,与Parallel Scavenge收集器搭配使用(因为Parallel Old在JDK6才发布)。
2、作为CMS收集器发生失败时的备选方案。在并发收集发生Concurrent Mode Failure时使用。
Serial+Serial Old工作流程如图所示:
5、Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持多线程并发收集。也是基于标记-整理算法。
这个收集器是JDK6才提供的。在之前的版本中,如果想使用Parallel Scavenge收集器,老年代只有Serial Old收集器可供选择。其他表现良好的收集器,如CMS无法搭配他使用(无法搭配使用的原因在本章ParNew收集器中介绍)。
由于Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。
直到Parallel Old的出现,“吞吐量优先”收集器才有了名副其实的搭配组合。在注重吞吐量或者处理器资源比较稀缺的场合,都可以优先考虑Parallel Scavenge+Parallel Old收集器这个组合。
Parallel Scavenge+Parallel Old的工作流程如下:
6、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。常常用来注重服务的响应速度这类应用上的。与前面介绍的5中收集器都不同,CMS采用的是标记-清楚算法。
CMS的整个运行过程,相比较于前面的几种收集器,更加的负责,整个过程可以分为以下4个步骤:
1、初始标记(CMS initial mark)
2、并发标记(CMS concurrent mark)
3、重新标记(CMS remark)
4、并发清楚(CMS concurrent sweep)
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记只是标记一下GC Roots能直接到达的对象,速度很快。并发标记阶段就是从GC Roots的直接关联对象遍历整个对象图的过程,这个过程耗时比较长,但是不需要停顿用户线程。重新标记阶段,则是为了修正在并发标记阶段由于程序运行而产生变动的那一部分标记记录。这个阶段的停顿时间通常会比初始阶段稍长一些,但是也远比并发标记阶段短。最后是并发清除阶段,清理删除掉标记阶段判断死亡的对象,由于不需要移动存活对象,所以这个阶段可以和用户线程一起执行。
在整个CMS的工作流程中,并发标记和并发清除是耗时最长的,在其他时候,垃圾收集器都是可以与用户线程一起工作的。所以从总体上来看,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS的执行流程入下图所示:
由于CMS在并发清理阶段,用户线程是可以正常运行的,程序正常运行期间会产生新的垃圾对象。且这些新的垃圾对象时在标记之后产生的,因此CMS无法在当次收集时清理掉他们。只能留待下一次垃圾清理时清理。这一部分垃圾被称为“浮动垃圾”。
同样的,由于用户线程是正常运行的,那就需要预留够足够的空间,以便用户线程使用。也就是说CMS不能在老年代几乎被填满的时候,进行垃圾收集,必须留一部分空间供并发收集时的程序使用。在JDK5的默认设置中,CMS收集器当老年代使用了68%的空间后就被激活。这是一个保留的设置,当老年代增长不是太快时,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高CMS的触发百分比,降低内存的回收频率,获取更好的性能。
到了JDK6,默认的百分比已经提高到了92%。但这又面临另一种风险:要是CMS运行期间预留的内存空间无法满足程序分配对象的需要,就会出现一次“并发失败(Concurrent Mode Failure)”,这时候虚拟机就会启动后背预案:冻结用户线程,临时采用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间会很长。因此参数-XX:CMSInitiatingOccupancyFraction设置的太高将会容易导致大量的并发失败产生,性能反而会降低。
CMS采用的是并发清理算法,这个算法有个最大的缺点:会产生大量的空间碎片。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现,命名老年代有很多剩余空间,但是无法找到足够大的连续空间来分配当前对象,而不得不触发一次FULL GC。