垃圾收集器就是收集算法的具体实现,不同的虚拟机会提供不同的垃圾收集器。并且提供参数供用户根据自己的应用特点和要求组合各个年代所使用的收集器。本文讨论的收集器基于Sun Hotspot虚拟机1.6版。 下图中展示了jdk1.6中提供的6种作用于不同年代的收集器,两个收集器之间存在连线的话就说明它们可以搭配使用。没有最好的收集器,也没有万能的收集器,只有最合适的收集器。从Serial收集器到Parallel收集器,再到CMS收集器, G1收集器,用户线程的停顿时间在不断缩短,但是仍然没有办法完全消除。
1. Serial收集器
单线程收集器,使用复制收集算法,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World),直到收集结束,虚拟机运行在Client模式时的默认新生代收集器。 优点是:简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器没有现成交互的开销。在堆比较小的情况下,一般停顿时间很短,是可以使用这种收集器的。如下图:
2. ParNew收集器
ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。ParNew收集器是许多运行在server模式下的虚拟机中首选的新生代收集器(一个原因是在除了serial收集器外,目前只有它能与CMS收集器配合使用)。
ParNew收集器是使用-XX:+UseConcMarkSweepGC选项的默认新生代收集器;也可以用-XX:+UseParNewGC选项来强制指定它。ParNew收集器在单CPU环境中不比Serial效果好,甚至可能更差,两个CPU也不一定跑的过,但随着CPU数量的增加,性能会逐步增加。默认开启的收集线程数与CPU数量相同。在CPU数量很多的情况下,可以使用-XX:ParallelGCThreads参数来限制线程数。如下图
3. Parallel Scavenge收集器
同ParNew一样是使用复制算法的新生代并行多线程收集器。
Parallel Scavenge的特点是它的关注点与其他收集器不同,CMS等收集器的关注点尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。也被称为吞吐量优先收集器。所谓吞吐量就是CPU用于运行用户代码时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间/运行用户代码时间+垃圾收集时间。
高吞吐量和停顿时间短的策略相比,主要强调任务更快完成,适用于后台运算而不需要太多交互的任务;而后者强调用户交互体验。
Parallel Scavenge提供两个参数精确控制吞吐量,-XX:MaxGCPauseMillis控制最大垃圾收集停顿时间和-XX:GCTimeRatio设置吞吐量大小
MaxGCPauseMillis允许的值是一个大于零的毫秒数,收集器将尽力保证内存回收话费的时间不超过设定值。GC停顿时间缩小是以牺牲吞吐量和新生代空间来换取的,也就是要使停顿时间更短,需要使新生代的空间减小,这样垃圾回收的频率会增加,吞吐量也降下来了。
GCTimeRatio的值是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率。默认为99,则允许最大GC时间就占总时间的1%(1/(1+99)).还有一个参数,-XX:+UseAdaptiveSizePolicy,是个开关参数,打开后会自动调整Eden/Survivor比例,老年代对象年龄,新生代大小等。这个参数也是Parallel Scavenge和ParNew的重要区别。
4. Serial Old收集器
是Serial的老年代版本,同样是单线程收集器,使用标记-整理算法。主要是client模式下的虚拟机使用。参考上面图Serial/Serial old.
两大用途:在JDK1.5及之前的版本中与Parallel Scavenge搭配使用;作为CMS收集器的后备预案。在并发收集发生Concurrent Mode Failure时使用。
5. Parallel Old收集器
是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。在JDK1.6中才开始使用。由于之前的版本中,Parallel Scavenge只有使用Serial Old作为老年代收集器,其吞吐量优先的设计思路不能被很好的贯彻,在Parallel Old收集器出现后,这两者的配合主要用于贯彻这种思路。如下图:
6. CMS收集器
Concurrent Mark Sweep 以获取最短回收停顿时间为目标的收集器,比较理想的应用场景是B/S架构的服务器。如下图:
基于标记-清除算法实现,运行过程分成4个步骤:
a)初始标记(需要stop the world),标记一下GC Roots能直接关联到的对象,速度很快
b)并发标记,进行GC Roots Tracing的过程。
c)重新标记(需要stop the world),为了修正并发标记时用户继续运行而产生的标记变化,停顿时间比初始标记长,远比并发标记短。
d)并发清除
缺点:
CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量就会降低。CMS默认启动的回收线程数为(CPU数量+3)/4。为了解决这一情况,有一个变种i-CMS,但目前并不推荐使用。
CMS收集器无法处理浮动垃圾(floating garbage)。同样由于CMS GC阶段用户线程还需要运行,即还需要预留足够的内存空间供用户线程使用,因此CMS收集器需要预留一部分空间提供并发收集时的程序运作使用。默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活。这个值可以用-XX:CMSInitiatingOccupancyFraction来设置。要是CMS运行期间预留的内存无法满足程序需要,就会出现concurrent mode failure,这时候就会启用Serial Old收集器作为备用进行老年代的垃圾收集。
空间碎片过多(标记-清除算法的弊端),提供-XX:+UseCMSCompactAtFullCollection参数,应用于在FULL GC后再进行一个碎片整理过程。-XX:CMSFullGCsBeforeCompaction,多少次不压缩的full gc后来一次带压缩的。
7. G1收集器
G1收集器与前面的CMS收集器相比有两个显著的改进:一是G1收集器是基于“标记-整理”算法实现的收集器,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。二是它可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代、老年代)划分为多个大小固定的独立区域(Region),并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,优先回收垃圾最多的区域(这就是Garbage First名称的来由)。区域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集效率。