如果说垃圾收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于Sun HotSpot虚拟机1.6版Update 22,这个虚拟机包含的所有收集器如下图所示。
image.png
图中列出了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
Serial收集器
Serial收集器是最基本、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机新生代收集的唯一选择。
缺点
- 这个收集器是一个单线程的收集器。
- 在它进行垃圾收集时,必须暂停其它所有的工作线程(Stop The World),直到它收集结束。
优点
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。
应用场景
到现在为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代,停顿时间完全可以控制在几十毫秒,最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
JVM参数
- -XX:+UseSerialGC:打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收。
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。实现上这两种收集器也共用了相当多的代码。
缺点
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证能超越Serial收集器。
优点
随着可以使用的CPU的数量的增加,ParNew收集器对于GC时系统资源的利用很有好处。ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下(比如32个),可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
应用场景
ParNew收集器除了多线程收集之外,其它与Serial收集器相比并没有太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。CMS作为老年代的收集器,无法与在JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作(原因是Parallel Scavenge收集器及G1收集器都没有使用传统的GC收集器代码框架,而是另外独立实现。),所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。
JVM参数
- -XX:ParallelGCThreads:设置并行GC时进行内存回收的线程数(-XX:ParallelGCThreads=16)。
- -XX:+UseParNewGC:打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收。
Parallel Scavenge收集器
Parallel Scavenge收集器跟ParNew收集器一样也是一个多线程的新生代收集器,也是使用复制算法。
特点
Parallel Scavenge收集器的特点是它的关注点与其它收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所以Parallel Scavenge收集器也经常被称为“吞吐量优先”收集器。
吞吐量
吞吐量是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码的时间 / (运行用户代码的时间 + 垃圾收集时间)。Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数及直接设置吞吐量大小的-XX:GCTimeRatio参数。
应用场景
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
JVM参数
- -XX:+UseParallelGC:虚拟机运行在Server模式下的默认值。打开此开关后,使用Parallel Scavenge + Serial Old (PS MarkSweep)的收集器组合进行内存回收。
- -XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间,仅在使用Parallel Scavenge收集器时生效。这个参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值。GC停顿时间的缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代空间调小些,垃圾收集就会比较快,但是这也直接导致垃圾收集发生得更频繁,吞吐量变小。
- -XX:GCTimeRatio:设置GC时间占CPU总时间的比率,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge收集器时生效。这个参数允许的值是一个大于0小于100的整数,如果把此参数设为19,那允许的最大GC时间就占总时间的5%(即1 / (1 + 19))。
- -XX:+UseAdaptiveSizePolicy:动态调整Java堆中各个区域的大小以及进入老年代的年龄。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:MaxTenuringThreshold)等细节参数了,只需要把基本的内存参数设置好(如-Xmx),然后使用MaxGCPauseMillis或GCTimeRatio参数给虚拟机设立一个优化目标,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整具体的细节参数以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器和ParNew收集器的一个重要区别。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
应用场景
Serial Old收集器主要也是用于Client模式下的虚拟机。如果在Server模式下,它主要有两大用途:
在JDK 1.5及之前的版本中与Parallel Scavenge收集器搭配使用。Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并没有直接使用Serial Old收集器,但是PS MarkSweep收集器是以Serial Old收集器为模板设计的,与Serial Old的实现非常接近。
作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
出现的原因
这个收集器是JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器只能和Serial Old(PS MarkSweep)收集器配合工作。由于单线程的老年代Serial Old收集器在服务端应用性能上的“拖累”,即便使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,又因为老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。
应用场景
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。
JVM参数
- -XX:+UseParallelOldGC:打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收。
CMS(Concurrent Mark Sweep)收集器
CMS收集器是HotSpot在JDK 1.5中推出的第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作,是一种以获取最短回收停顿时间为目标的收集器。
工作原理
CMS收集器是基于“标记-清除”算法实现的,它的工作流程可以分为4个步骤:
- 初始标记(CMS initial mark):初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。初始标记仍然需要“Stop The World”。
- 并发标记(CMS concurrent mark):并发标记阶段就是进行GC Roots Tracing的过程。
- 重新标记(CMS remark):重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那部分对象的标记记录。这个阶段也需要“Stop The World”,它的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 并发清除(CMS concurrent sweep):并发清除阶段就是清除死亡对象的过程。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以和用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。
优点
CMS是一款优秀的收集器,它的主要优点是并发收集、低停顿。Sun的一些官方文档里称它为并发低停顿收集器(Concurrent Low Pause Collector)。
缺点
主要有三个显著的缺点:
- CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量降低。CMS默认启动的回收线程数是(CPU数量 + 3) / 4,当CPU数量在4个以上时,并发回收时垃圾收集线程最多占用不超过25%的CPU资源;但是当CPU不足4个时(比如2个),那么CMS对用户程序的影响就可能变得很大。为了解决这种情况,虚拟机提供了一种称为“增量式并发收集器(Incremental Concurrent Mark Sweep / i-CMS)”的CMS收集器变种。这种收集器采用跟单CPU年代PC操作系统使用抢占式来模拟多任务机制一样的思想,在并发标记和并发清除的时候让GC线程、用户线程交替运行,尽量减少GC线程独占资源的时间,这样整个垃圾收集的过程会变长,但对用户程序的影响会显得少一些,速度下降没那么明显。但是目前版本中,i-CMS已经被声明为“deprecated”。
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的发生。由于CMS并发清理阶段用户线程还在运行着,有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好留待下一次GC时再将它们清除,这部分垃圾就称为“浮动垃圾”。也是由于垃圾收集阶段用户线程还在运行,CMS收集器需要预留足够的内存空间给用户线程使用,不能等老年代几乎被填满才进行收集。在默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数以获取更好的性能。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这是虚拟机将启动后备预案,临时启动Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction的值设置得太高很容易导致大量的“Concurrent Mode Failure”失败,性能反而降低。
- CMS收集器是基于“标记-清除”算法的,所以收集结束时会产生大量的空间碎片。空间碎片过多时,如果无法为大对象找到足够大的连续空间,就不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数,用于在Full GC后再进行一次碎片整理。内存整理过程是无法并发的,所以空间碎片问题没有了,但是停顿时间变长了。因而,虚拟机还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。
应用场景
CMS收集器非常适合B/S系统服务端这类重视服务响应速度,希望系统停顿时间最短,以给用户带来较好体验的应用。
JVM参数
- -XX:UseConcMarkSweepGC:打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。
- -XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效。
- -XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否进行一次内存碎片整理,仅在使用CMS收集器时生效。
- -XX:CMSFullGCsBeforeCompaction:设置CMS收集器在进行多少次垃圾收集后是再进行一次内存碎片整理,仅在使用CMS收集器时生效。
G1(Garbage First)收集器
G1收集器是当前收集器技术发展的最前沿成果。
优点
G1收集器与CMS收集器相比有三个显著的改进:
- G1收集器可以在基本不牺牲吞吐量的前提下完成低停顿的内存回收。
- G1收集器是基于“标记-整理”算法实现的,所以不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。
- 它可以非常精确地控制停顿,即能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
工作原理
相对于其它收集器对整个新生代或老年代进行垃圾收集,G1收集器极力地避免全区域的垃圾收集。G1收集器将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(Garbage First名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。
其它的JVM参数
- -XX:PretenureSizeThreshold:设置直接晋升到老年代的对象大小,大于这个参数的对象将直接在老年代分配(设置这个参数的值为3M:-XX:PretenureSizeThreshold=3145728)。
- -XX:MaxTenuringThreshold:设置晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就加1,当超过这个参数值时就进入老年代(-XX:MaxTenuringThreshold=10)。
- -XX:+HandlePromotionFailure:打开这个开关,虚拟机可以允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况。
- -XX:+PrintGCDetails:打开这个开关,虚拟机在发生垃圾收集行为时会打印内存回收日志,并且在进程退出的时候输出当前内存各区域的分配情况。
网友评论