JVM提供了以下四种不同的垃圾收集算法:
Serial垃圾收集器是四种垃圾收集器中最简单的一种。如果应用运行在Client型虚拟机(Windows平台上的32位JVM或者是运行在单处理器机器上的JVM)上,这也是默认的垃圾收集器。
Serial收集器使用单线程清理堆的内容。使用Serial收集器,无论是进行Minor GC还是Full GC,清理堆空间时,所有的应用线程都会被暂停。进行Full GC时,它还会对老年代空间的对象进行压缩整理。通过-XX:+UseSerialGC
标志可以启用Serial收集器(大多数情况下,如果可以使用这个标志,默认就会开启)。注意,跟大多数的JVM标志不同,关闭Serial收集器不能简单地将加号符变成减号符(譬如,使用-XX:-UseSerialGC
)。在Serial收集器作为默认收集器的系统上,如果需要关闭Serial收集器,可以通过指定另一种垃圾收集器来实现。
Throughput收集器是Server级虚拟机(多CPU的Unix机器以及任何64位虚拟机)的默认收集器。
Throughput收集器使用多线程回收新生代空间,Minor GC的速度比使用Serial收集器快得多。处理老年代时Throughput收集器也能使用多线程方式。这已经是JDK 7u4
及之后的版本的默认行为,对于之前老版本的JDK7
虚拟机,通过-XX:+UseParalleloldGc
标志可以开启这个功能。由于Throughput收集器使用多线程,Throughput收集器也常常被称为Parallel收集器。 Throughput收集器在Minor GC和Full GC时会暂停所有的应用线程,同时在Full GC过程中会对老年代空间进行压缩整理。由于在大多数适用的场景,它已经是默认的收集器,所以基本上不需要显式地启用它。如果需要,可以使用-XX:+UseParallelGC
、-XX:+UseParalleloldGc
标志启用Throughput收集器。
CMS收集器设计的初衷是为了消除Throughput收集器和Serial收集器Full GC周期中的长时间停顿。CMS收集器在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。然而,这其中最显著的不同是,CMS不再使用Throughput的收集算法(-XX:+UseParallelGC
),改用新的算法来收集新生代对象(使用-XX:+UseParNewGC
标志)。
CMS收集器在Full GC时不再暂停应用线程,而是使用若干个后台线程定期地对老年代空间进行扫描,及时回收其中不再使用的对象。 这种算法帮助CMS成为一个低延迟的收集器:应用线程只在Minor GC以及后台线程扫描老年代时发生极其短暂的停顿。应用程序线程停顿的总时长与使用Throughput收集器比起来短得多。
额外付出的代价是更高的CPU使用:必须有足够的CPU资源用于运行后台的垃圾收集线程,在应用程序线程运行的同时扫描堆的使用情况。除此之外,后台线程不再进行任何压缩整理的工作,这意味着堆会逐渐变得碎片化。如果CMS的后台线程无法获得完成他们任务所需的CPU资源,或者如果堆变得过度碎片化以至于无法找到连续空间分配对象,CMS就蜕化到Serial收集器的行为:暂停所有应用线程,使用单线程回收、整理老年代空间。 这之后又恢复到并发运行,再次启动后台线程(直到下一次堆变得过度碎片化)。通过-XX:+UseConcMarkSweepGC
、-XX:+UseParNewGC
标志(默认情况下,这两个标志都是禁用的)可以启用CMS垃圾收集器。
G1垃圾收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于4GB)时产生的停顿。G1收集算法将堆划分为若干个区域(Region),不过它依旧属于分代收集器。这些区域中的一部分包含新生代,新生代的垃圾收集仍然采用暂停所有应用线程的方式,将存活对象移动到老年代或者Survivor空间。同其他的收集算法一样,这些操作也利用多线程的方式完成。
G1收集器属于Concurrent收集器:老年代的垃圾收集工作由后台线程完成,大多数的工作不需要暂停应用线程。由于老年代被划分到不同的区域,G1收集器通过将对象从一个区域复制到另一个区域,完成对象的清理工作,这也意味着在正常的处理过程中,G1收集器实现了堆的压缩整理(至少是部分的整理)。因此,使用G1收集器的堆不大容易发生碎片化——虽然这种问题无法避免。
同CMS收集器一样,避免Full GC的代价是消耗额外的CPU周期:负责垃圾收集的多个后台线程必须能在应用线程运行的同时获得足够的CPU运行周期。通过标志-XX:+UseG1GC
(默认值是关闭的)可以启动G1垃圾收集器。
触发及禁用显式的垃圾收集
通常情况下垃圾收集是由JVM在需要的时候触发:新生代用尽时会触发Minor GC,老年代用尽时会触发Full GC,或者堆空间即将填满时会触发Concurrent垃圾收集(如果情况需要)。
Java也提供了一种机制让应用程序强制进行GC:这就是System.gc()
方法。通常情况下,试图通过调用这个方法显式触发GC都不是个好主意。调用这个方法会触发FullGC(即使JVM使用CMS或者G1垃圾收集器),应用程序线程会因此而停顿相当长的一段时间。同时,调用这个方法也不会让应用程序更高效,它会让GC更早地开始,但那实际只是将性能的影响往后推迟而已。
任何原则都有例外,尤其是在做性能监控或者基准测试时。运行少量的代码进行基准测试时,为了更快地预热JVM,在测量周期之前强制进行一次GC还是有意义的。类似的情况还包括在进行堆分析时,通常在获取堆转储之前,强制进行一次Full GC是一个不错的主意。虽然大多数抓取堆转储的方法都能进行Full GC,也存在其他的方法可以强制进行Full GC:你可以通过执行jcmd<进程号>GC.run
,或者使用jconsole连接到JVM在内存面板上单击“进行GC”按钮。
另一个例外是RMI,作为它分布式垃圾收集器的一部分,每隔一小时它会调用System.gc()
一次。这里的调用时间可以通过系统属性中的-Dsun.rmi.dgc.server.gcInterval=N
和.Dsun.rmi.dgc.cli ent.gcInterval=N
进行修改。N值的单位以毫秒记,在Java7
(该值与之前的版本亦不相同)中的默认值为3600000(即一个小时)。
如果你运行的程序调用的第三方代码中错误地调用了System.gc()
方法,可以通过JVM参数-XX:+DisableExplicitGC
显式地禁止这种类型的GC;默认情况下该标志是关闭的。
快速小结
GC算法的选择一方面取决于应用程序的特征,另一方面取决于应用的性能目标。
Serial收集器最适用于应用程序的内存使用少于100MB的场景。 这种情况下应用程序只需要很小的堆,无论是Throughput收集器的并行收集,还是CMS收集器或G1收集器的后台收集都发挥不了太大的作用。
这个Sizing准则也限制了Serial收集器的使用范畴。大多数的程序需要在Throughput和Concurrent收集器之间做出抉择;而选择的依据大多数情况下是由应用程序的性能目标所决定的m, 不同应用在耗时、吞吐量、或者平均(或者总量90%的)响应时间上的要求迥异。
对批量任务而言,Throughput收集器所引入的停顿,尤其是FulI GC的停顿是主要的顾虑。每个任务的执行都会为总的执行时间增加一部分的延迟时间(elapse time)。如果每次Full GC耗时0.5秒,程序5分钟的运行时间内要进行20个这样的周期,那么性能的损耗就高达3.4%:如果没有这些停顿,程序可以在290秒而不是300秒内完成运行。
如果有额外的CPU处理能力(这很可能是个问题),那么使用Concurrent收集器将极大地提升应用程序的性能。 这里的关键在于我们能否提供足够的CPU给Concurrent收集器的线程进行后台的处理工作。举个简单的例子:一个单CPU的机器上,单线程的应用程序已经消耗了100%的CPU资源。该应用程序使用Throughput收集器运行时,GC间歇性地发生,导致应用程序线程出现停顿。同样的程序,如果切换到Concurrent收集器,操作系统一会在CPU上运行应用程序线程,一会儿运行GC的后台线程。最终的结果是一样的:操作系统运行其他线程时,应用程序线程依然会发生停顿(不过有可能是更短时间的停顿)。
这个原则同样适用于通用情况,即多个应用程序线程、多个后台GC线程运行于多CPU的系统上。如果操作系统无法在同时运行所有应用程序线程和GC后台线程,那么对CPU的竞争就会反映到应用程序线程的停顿上。
上面的图片展示了这个取舍是如何工作的。计算股票数据的批量应用已经运行于特定的模式,它们会将结果集保持在内存中数分钟(目的是填满整个堆);测试分别使用了CMS和Throughput垃圾收集算法。
快速小结
。