最近刚好有时间,就简单的看了下JVM的几种垃圾回收器,它们都是计算机历史发展的产物,先简单的做一个整理,并没有哪一款垃圾收集器就一定是最优,还需要结合使用场景、参数配置等进行考量,根据系统情况搭配出尽可能合理优质的垃圾回收策略,而这往往需要经验的积累;
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。
下图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
并发与并行
这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下。
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
Serial收集器 [ˈsɪəriəl]
Serial 收集器是最基本、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是虚拟机年轻代收集的唯一选择。
特性
年轻代,单线程收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程(Stop the world),直到它收集结束。
优势
简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
应用场景
在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集儿十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial 收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
参数控制
-XX:+UseSerialGC -- 串行收集器
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本;
特性
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。
优势
随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。
它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX: ParaIlelGCThreads参数来限制垃圾收集的线程数。
劣势
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。
应用场景
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。
参数控制
-XX:+UseParNewGC -- ParNew 年轻代收集器
-XX:ParaIlelGCThreads -- 限制垃圾收集的线程数
备注
除了Serial收集器外,目前只有它能与CMS收集器配合工作。
Parallel Scavenge收集器 [ˈpærəlel] [ˈskævɪndʒ]
特性
相比其他收集器,Parallel收集器更关注系统的吞吐量。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
优势
GC自适应的调节策略,当使用-XX:+UseAdaptiveSizePolicy参数后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,只需要把基本的内存数据设置好(如-Xmx),然后使用MaxGCPauseMillis参数或GCTimeRatio参数给VM设立一个优化目标,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。
应用场景
如果对于收集器运作原理不太了解,手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个很不错的选择。
参数控制
-XX:+UseParallelGC -- 使用Parallel收集器 + 老年代串行
-XX:+UseAdaptiveSizePolicy -- GC自适应的调节策略
收集器提供了两个参数用于精确控制吞吐量:
-XX:MaxGCPauseMillis -- 最大垃圾收集停顿时间,是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过设定值,但是GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的;
-XX:GCTimeRatio -- 直接设置吞吐量大小();默认值为99,就是允许最大1%的垃圾收集时间。
Serial Old收集器
Serial Old是Serial收集器的老年代版本。
特性
老年代,单线程收集器,使用“标记-整理”算法。
优势
应用场景
①Client模式
Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。
②Server模式
如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
参数控制
备注
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本。
特性
老年代,多线程收集器,使用“标记—整理”算法,该收集器在JDK1.6中才开始提供。
优势
应用场景
在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge + Parallel Old收集器两者的组合。
参数控制
-XX:+UseParallelOldGC -- 使用Parallel收集器+ 老年代并行
CMS收集器
CMS(Concurrent Mark Sweep)收集器,是一种以获取最短回收停顿时间为目标的收集器。
特性
从名字(包含“Mark Sweep”)上就可以看出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收集器对CPU资源非常敏感:其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
②CMS收集器无法处理浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉,这一部分垃圾就称为“浮动垃圾”。
③CMS收集器会产生大量空间碎片:CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了两个开关参数-XX:+ UseCMSCompactAtFullCollection && -XX:+CMSFullGCsBeforeCompaction;
应用场景
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
参数控制
-XX:+UseConcMarkSweepGC -- 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection -- 用于在“享受”完FullGC之后,免费附赠一次碎片整理过程,由于内存整理的过程是无法并发的,所以时间会比较长;
-XX:+CMSFullGCsBeforeCompaction -- 用于执行完N次不压缩FullGC之后,跟着来一次带压缩的;
-XX:ParallelCMSThreads(后期又称为ConcGCThreads) -- 定义并发CMS过程运行时的线程数。如果该标志未设置,JVM会根据并行收集器中的-XX:ParallelGCThreads参数的值来计算出默认的并行CMS线程数。ParallelCMSThreads = (ParallelGCThreads + 3)/4,ParallelGCThreads默认会被设置为CPU的数量;
G1收集器
G1收集器是垃圾收集器理论进一步发展的产物,HotSpot开发团队赋予它的使命是未来可以替换掉JDK 1.5中发布的CMS收集器。
G1收集器打破了年轻代与老年代的物理界限。
取而代之的是,G1算法将堆划分为若干个区域(Region),它仍然属于分代收集器。如下图所示,堆被分为 Eden、Survivor、old generation区。此外,还有第四种类型的对象被称为巨无霸区域(Humongous regions),用来保存比标准块(standard region)大50%及以上的对象,它们存储在一组连续的区中,最后一个类型是堆内存中的未使用区(unused areas).
特性
基于“标记一整理”算法,可独立完成分代收集,打破年轻代与老年代的物理界限,可预测停顿;
优势
①不会产生内存碎片:基于“标记一整理”算法,也就是说它不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。同时也不会因为分配大对象时无法找到连续内存空间而提前触发下一次GC。
②可预测停顿:它可以非常精确地控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上花费的时间不得超过N毫秒,这几乎已经是实时Java (RTSJ)的垃圾收集器的特征。
应用场景
可以像CMS收集器一样,GC操作与应用的线程一起并发执行;
需要可预测的GC暂停耗时;
参数控制
-XX:+UseG1GC -- 启用G1收集器
-XX:MaxGCPauseMillis -- 设置GC的最大暂停时间(单位:毫秒)
-XX:InitiatingHeapOccupancyPercent -- 启动并发GC时的堆内存占比,基于整个堆的使用率,而不只是某一代内存的使用比例;默认值为 45;值为 0 则表示“一直执行GC循环“;
备注
①不要设置年轻代的大小(Young Generation Size),假若通过 -Xmn 显式地指定了年轻代的大小, 则会干扰到 G1收集器的默认行为;
②设置 MaxGCPauseMillis 时不应该使用平均响应时间作为指标,而应该考虑使用目标时间的90%或者更大作为响应时间指标.。也就是说90%的用户请求响应时间不会超过预设的目标值,不过该暂停时间只是一个目标,并不能保证总是得到满足。
参考:
《深入理解Java虚拟机:JVM高级特性与最佳实践_周志明》
java垃圾回收器:http://www.jianshu.com/p/50d5c88b272d
http://www.cnblogs.com/ityouknow/p/5614961.html