1.基本收集算法

1.标记清除(mark-sweep):算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记清除算法是最基本的收集算法,它后面的算法都是在它的基础上并对它的缺点进行改进而得到的。它的缺点主要有两个:一个效率问题,标记和清除过程的效率都不高,因为他要该遍历整个堆空间,成本较大暂停时间随空间大小线性增大;另一个是空间问题,标记清除后会产生大量的内存碎片,碎片太多可能导致,当程序的运行过程中需要分配较大的对象时无法找到连续的内存空间而不得不提前触发另一次的垃圾收集动作。


2.复制:为了解决效率问题,“复制”(Copying)收集算法出现了,它将可用内分成两个大小相同的块,每次只使用其中的一块。当这块内存使用完了,就将活着的对象复制到另外一块上面,然后将使用过的内存空间一次性的回收。这样使得每次都是对其中的一块内存进行回收,内存分配也就不用考虑内存碎片等复杂情况。只需要移动堆顶指针,按顺序分配即可,实现简单,运行高效。只是这种算法的代价是将可用内存缩小到原来的一半,代价有点高。

   现代的商业虚拟机都是采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和2块较小的Survivor空间,每次使用Eden和其中的一块Survivor.当回收时,将Eden和Survivor中还存活的对象一次性的拷贝到另外的一块Survivor上,最后清理掉Eden和刚才使用过的Survivor。HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代空间的90%(80% + 10%),只有10%的空间会被空置。当然98%的对象可以回收是一搬场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其它的内存(这里指老年代)进行分配担保(Handle Promotion)。也就是将对象直接在老年代分配。


3.标记整理(mark-sweep-compact):复制收集算法在对象的存活率比较高的情况下会进行较多的复制操作,效率将会变的低。更关键的是,如果不想浪费一块Survivor的空间,就需要额外的空间进行分配担保,以应对被使用的内存中所有对象100%存活的极端情况,所以在老年代一半不能直接采用这种算法。

   更加老年代的特点,出现了“标记-整理”(Mark-Compact)算法,标记的过程和“标记-清除”算法一样,但后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向一段移动,然后直接清理掉边界以外的内存。

2.分代收集

   当代虚拟机的垃圾收集都是采用“分代收集”(Generational Collection),这种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。一般是把Java堆划分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法。在新生代,每次垃圾收集都发现有大量的对象死去,只有少量的存活,就可以采用复制算法,老年代因为对象的存活率高,没有额外的内存空间对它进行分配担保,就必须采用“标记-清理”或“标记-整理”算法来进行回收。



三、收集器


如果说垃圾收集算法内存回收的方法论,则垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现没有任何规定,因此不同的厂商,不同的版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据自己应用的特点组合出各个年代所使用的收集器。下面讨论的是基于Sun HotSpot虚拟机的1.6版Update22。这个虚拟机包含的所有收集器如图:

JVM垃圾收集算法和垃圾收集器_java jvm gc



   上图展示了7中用于不同年代的收集器(包括JDK 1.6_Update14后引入的Early Access版G1收集器),如果两个收集器之间存在连线,就说明它们可以搭配使用。


Serial收集器


Serial收集器是最基本,历史最悠久的收集器,曾经是虚拟机(在JDK 1.3.1之前)新生代收集器的唯一选择。这是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CUP或一条收集线程完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其它的所有的工作线程(Sun将这件事情成为“Stop The World”),直到它收集结束。“Stop The World”这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉,这对有些应用来说是不可接受的。下图表示Serial/Serial Old收集器的工作过程:

JVM垃圾收集算法和垃圾收集器_java jvm gc_02


   从JDK 1.3开始,HotSpot虚拟机的开发团队为消除或减少工作线程因内存回收而导致的停顿一直努力着,从Serial到Parallel,再到Concurrent Mark Sweep(CMS)到没有Garbage First(G1)收集器,我们看到了一个个越来越优秀的的收集器的出现,用户线程的停顿时间在缩短,但还是没有办法完全消除(这里不包括RTSJ中的收集器),寻找更优秀的垃圾收集器的工作仍在继续。

   虽然Serial收集有其缺点,但到目前为止,它依然是运行在Client模式下默认的新生代收集器。它也有优点:简单高效(与其它收集器的单线程比),对与单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。


ParNew收集器


ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数,收集算法,Stop The World,对象分配规则,回收策略等都与Serial收集器完全一样。下图表示ParNew/Serial Old收集器工作的过程:

JVM垃圾收集算法和垃圾收集器_java jvm gc_03


   ParNew收集器出了多线程收集外,其它与Serial收集器相比并没有太多的创新,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器。其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。


Paralle Scavenge收集器


   Paralle Scavenge收集器也是一个新生代的收集器,它也是用于复制收集算法的收集器,又是并行的多线程收集器。它与ParNew收集器不同的地方在于它的关注点与其它收集器不同:Paralle Scavenge收集器的目标是达到一个可以控制吞吐量。所谓吞吐量就是CUP用于运行用户代码的时间与CPU总的消耗时间的比值。

   停顿时间越短就越适合与用户交互的程序,良好的响应速度能提示用户的体验;而高吞吐量则可以最高效率的利用CPU的时间,尽快的完成程序的运算任务,主要使用与在后台运算而不需要太多交互的任务。

   Paralle Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的:-XX:MaxGCPauseMillis参数及直接设置吞吐量大小 -XX:GCTimeRation.

   MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存的回收时间不超过设定的值。GC的停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小点,收集300M新生代肯定比收集500M快,这也直接导致垃圾收集发生的更频繁一些,原来10秒收集一次,每次停顿100毫秒,现在变为5秒收集一次,每次停顿70毫秒,停顿时间在下降,但吞吐量也下来了。

   GCTimeRatio参数的值应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率。相当与吞吐量的倒数。如果把次参数设置为19,那运行的最大GC时间占总时间的5%(即1/(1+19)),默认是99,即运行最大1%的垃圾收集时间。

   出了上面两个参数外,Paralle Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,打开后就不需要手工的指定新生代的大小(-Xmn),Eden和Survivor的比例(-XX:SurvivorRatio),晋升老年代对象的年龄(-XX:pretenureSizeThresHold)等细节参数了,虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间活最大的吞吐量,这种调节方式成为GC自适应的调节策略。自适应调节策略也是Paralle Scavenge收集器与ParNew收集器的一个重要区别。


Serial Old


   Serial Old是Serial收集器的老年代版本,它同样是一个单线程的收集器,使用“标记-整理”算法。这个收集器的主要意义也是被Client模式下的虚拟机使用如果在Server模式下,它还有2大主要用途:一个是在JDK 1.5及之前的版本中与Paralle Scavenge搭配使用,另外一个就是作为CMS收集器的后备预案,在并发收集器发生Concurrent Mode Failure的时候使用。


   Parallel Old 收集器


   Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的 Parallel Scanvege收集器一直处于比较尴尬的状态。原因是,如果新生代选择了 Parallel Scavenge收集器,老年代出了Serial Old(PS MarkSweep)收集器外别无选择。由于单线程的老年代Serial Old收集器在服务端应用性能上的“拖累”,即便使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量的最大化效果,又因为老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件又比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。

   直到 Parallel Old收集器出现后,“吞吐量优先”收集器忠于有了比较名副其实的应用组合,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。


   Parallel Scavenge 和 Parallel Old收集器工作过程如下图:

JVM垃圾收集算法和垃圾收集器_java jvm gc_04


CMS收集器


   CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前大部分的JAVA应用都是集中在互联网站或B/S系统的服务端,这类应用尤其注重服务的响应速度,希望系统停顿时间短,给用户给来流畅的体验。CMS收集器很符合这类应用的要求。

   CMS收集器使用“标记-清楚”算法,垃圾收集的过程非为4个阶段:


  • 初始标记

  • 并发标记

  • 重新标记

  • 并发清除




   其中初始标记,重新标记这两个步骤仍然需要STOP THE WORLD。初始标记仅仅是标记GC Roots能直接关联到的对象,速度很快,并发标记就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序运行而导致标记产生变动的那部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍微长一些,但远比并发标记的时间短。

   由于整个过程中耗时最长的是并发标记和并发清楚过程,收集器线程都可以与用户线程一起并发的执行。因此不会使服务的吞吐量下降太大。

   CMS收集器有如下的缺点:

   1:CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程的停顿,但是会因为占用了一部分CPU资源而导致应用程序变慢,总吞吐量会降低。CMS默认的收集线程是(CPU数量 + 3)/4,也就是当CPU在4个以上时,并发收集时垃圾收集线程最多占用不超过25%的CPU资源。但是当CPU不足4个,那么CMS对用户程序的影响就可能变得很大,如果CPU负载本来就比较大的时候,还分出去一部分运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低50%,这也很让人受不了,为了解决这种问题,虚拟机提供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器的变种,所做的事情和但CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记和并发清理的时候让GC线程,用户线程和GC线程交替运行,尽量减少GC线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,速度下降也就没有那么明显,但是目前的版本中,i-CMS已被声明为“deprecated",即不提倡用户使用。

   CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failue"失败而导致另一次的Full GC的产生。由于CMS并发清理阶段用户线程还在运行这,此时还会有新的垃圾产生,这部分垃圾在标记过程之后,CMS无法在本次垃圾收集中处理它们,只好留待下一次GC时再将其清理掉。也是由于在垃圾收集阶段用户线程还要运行,还需要预留足够的空间给用户线程,因此CMS收集器不能像其它收集器那样等到老年代几乎完全被填满了再进行垃圾收集,需要预留一部分空间在并发收集时供用户线程使用。在默认设置下,CMS收集器在老年代使用了68%的空间后就会被激活,这是一个保守的设置,如果应用在老年代增长的不是太快,可以适当的调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发的百分比,以便降低内存回收次数以获取更好的性能。要是CMS运行期间预留的内存无法满足程序的需要,就会出现一次“Concurrent Mode Failue”失败,这时虚拟机将启动后备方案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿的时间就更长了。这个参数的设置需要小心。

   还有一个缺点,CMS使用“标记-清除”算法,会产生内存碎片。当碎片过多的话,将会给大对象的分配带来麻烦,往往出现老年代还有很多空间,但是无法找到足够大的连续空间分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullColletion,用于在在Full GC后,提供一个碎片整理的过程,内存整理的过程是无法并发的。空间碎片问题没有了,但停顿的时间不得不变长了。虚拟机的设计者还提供了一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的。

   垃圾收集器参数总结:


参数名称参数描述
UseSerialGC
虚拟机运行在client模式下的默认值,打开次开关后,使用Serial+Serial Old 的收集器组合进行内存回收。
UseParNewGC打开次开关后,使用ParNew + Serial Old 的收集器组合进行内存回收。
UseConcMarkSweepGC打开次开关后,使用ParNew + CMS +Serial Old的收集器组合进行内存回收。Serial Old收集器将作为CMS收集器出现Concurrent Mode Failure失败收的后备收集器。
UseParallelGC
虚拟机运行在Server模式下的默认值,打开次开关后,使用ParallelScavenge + Serial Old的收集器组合进行内存回收。
UseParallelOldGC打开次开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收。
SurvivorRatio新生代中Eden区与Survivor区的容量比值,默认是8,代表Eden:Survivor = 8:1
PretenureSizeThreshold直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。
MaxTenuringThreshold晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就加1,当超过这个参数值时就进入到老年代。
UseAdaptiveSizePolicy动态调整Java堆中各个区的大小以及进入老年代的年龄。
HandlePromotionFailue
是否允许分配担保失败,即老年代空间不足以应对新生代的整个Eden和Survivor区的所有对象都存活的极端情况。
ParallelGCThreads设置并行GC时进行内存回收的线程数
GCTimeRatioGC时间占总时间的比率,默认值是99,仅在使用Parallel Scavenge收集器时生效。
MaxGCPauseMillis设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效。
CMSInitiatingOccupancyFraction设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认是68%,仅在使用CMS收集器时生效。
UseCMSCompactAtFullColletion设置CMS收集器在完成垃圾收集后是否必要进行一次内存碎片整理。仅在使用CMS收集器时生效。
CMSFullGCsBeforeCompaction设置CMS收集器在进行若干次垃圾收集后再启动一次内存碎片整理。仅在使用CMS收集器时生效。
-Xint

禁用JIT编译。

-Xmn
设置新生代的大小
-Xms设置初始堆的大小
-Xmx设置最大推的大小
-XX:PermSize设置永久代大初始大小
-XX:MaxPermSize设置永久代的最大值
-Xss设置虚拟机栈的大小。JDK1.5以前默认是256k,以后是1M
-XX:+DisableExplicitGC禁用System.gc()