垃圾收集入门

垃圾收集入门

主流垃圾收集器

Java主流的垃圾收集器分别是:

  • Serial收集器.
  • Parallel(Throughput)收集器.
  • Concurrent收集器,CMS.
  • Concurrent收集器,G1.

垃圾收集概述

Java最诱人的特性之一便是不需要显式地管理对象的生命周期,JVM会在后台自动回收不再使用的对象.
垃圾收集主要由两部分构成:

  • 查找不再使用的对象.
  • 释放这些对象所管理的内存
  • 有时候,还需要进行内存整理防止内存碎片.

在垃圾收集进行时,所有应用线程都停止运行所产生的的停顿被称为时空停顿(stop-the-world),通常这些停顿对应用的性能影响最大,调优垃圾收集时,尽量减少这种停顿是最为关键的因素.

分代垃圾收集器

虽然不同垃圾收集器实现细节千差万别,但都遵循同一个方式,即根据情况将堆分为不同的代(Generation),被分为老年代(Old Generation)和新生代(Young Generation),新生代又被分为Eden空间以及Survivor空间.

采用分代机制的原因是很多对象的生存周期非常短.采用分代设计主要有两个性能优势:

  • 新生代仅是堆的一部分,处理新生代更快.
  • 对象分配于新生代,垃圾收集时,新生代被清空,Eden空间对象要么被移走,要么被回收;所有存活对象要么被移动到老年代,要么被移动到Survivor空间,由于所有对象被移走,相当于新生代空间在垃圾收集时,自动进行了一次垃圾整理.

回收新生代空间,不用的对象被回收,存活的对象被移动到其他空间,这种操作称为Minor GC.
对象不断移动到老年代,最终老年代空间也会被占满,JVM需要找出老年代中不再使用的对象,并对它们进行回收,接着对对空间进行整理,这是垃圾收集算法差异最大的地方,这个过程称为Full GC,通常会导致应用程序线程长时间的停顿,

另一方面,通过更复杂的计算,我们还可能在应用程序运行的同时,找出不再使用的对象;CMS和G1收集器便是通过这种方式进行垃圾回收.由于它们不需要停止应用线程就能找出不再使用的对象,它们也被称为Concurrent收集器.Concurrent收集器采用不同的方法对老年代进行压缩.

使用CMS或G1收集器时,应用程序经历的停顿会更少(也更短),其代价便是消耗更多的CPU,CMS和G1收集器也可能遭受长时间的Full GC(调优算法以尽量避免出现这种情况).

评估垃圾收集器,要想达到整体的性能目标,每一个决定都需要取舍,如果应用对单个响应时间有要求,主要考虑以下方面:

  • 单个请求会受停顿时间的影响(受Full GC长时间停顿的影响更大),如果目标是尽可能地缩短响应时间,那么采用Concurrent收集器更为合适.
  • 如果平均响应时间比最大响应时间更为重要(譬如90%地响应时间),那么采用Throughput收集器通常就能满足需要.
  • 使用Concurrent收集器来避免长时间Full GC的代价便是消耗更多的CPU.

为批量应用选择垃圾收集器遵守以下规则:

  • 如果CPU足够强劲,使用Concurrent收集器避免发生Full GC可以让任务运行更快.
  • 如果CPU有限,那么Concurrent收集器额外的CPU消耗会让批量任务消耗更多的时间.

备注:

  • 所有GC算法都将堆划分为新生代和老年代.
  • 所有GC算法都在清理新生代对象时,都使用了”时空停顿”(Stop-The-World)方式收集垃圾,这通常是一个较快便能完成的操作.

GC算法

JVM提供四种不同的垃圾收集算法

Serial垃圾收集器

Serial收集是四种收集器中最简单的一种,如果运行在Client型虚拟机,这也是默认的垃圾收集器.
Serial收集器采用单线程清理堆内容,无论是Minor GC还是Full GC,所有应用线程都会被暂停,进行Full GC时,会对老年代的对象进行压缩整理,使用-XX:+UseSerialGC标志启用Serial收集器.

Throughput收集器

Thoughput收集器是Server虚拟机的默认收集器,Throughput收集器采用多线程回收新生代空间和老年代空间,Minor GC比Serial收集器快的多,Throughput收集器在Minor GC和Full GC时会暂停所有应用线程.在Full GC时会对老年代空间进行压缩整理.使用标志-XX:+UseParallelGC,-XX:+UseParallelOldGC启用Throughput收集器.由于Throughput收集器采用多线程,因此也被称为Parallel收集器.

CMS收集器

CMS收集器设计的初衷是为了解决Serial以及Throughput收集器Full GC周期长时间的停顿.CMS会在Minor GC中停止所有的应用线程,并采用多线程的方式回收新生代空间,不同于Throughput收集器的-XX:+UseParallelGC,改用新的算法来回收新生代对象(-XX:+UseParNewGC)

CMS在Full GC时,不再停止应用线程,而是使用多个后台线程定期地对老年代进行扫描,及时回收其中不再使用的对象.这种算法帮助CMS收集器成为一个低延迟的收集器.应用线程只在Minor GC以及后台线程扫描老年代时发生及其短暂的停顿.应用线程的停顿的总时长比Throughput收集器短得多.

这里需要付出的代价是更高的CPU使用:必须有足够的CPU资源用于后台的垃圾收集线程.
除此之外,后台线程不再进行任何压缩整理操作,这意味堆会逐渐变得碎片化,如果CMS的后台线程无法获得完成他们任务的CPU资源或者堆过于碎片化以至于无法找到连续空间分配对象.CMS就会蜕化到Serial收集器行为:暂停所有应用线程,使用单线程回收,整理老年代空间.这之后又恢复到并发运行,再次启动后台线程.使用-XX:+UseConcMarkSweepGC,-XX:+UseParNewGC启用CMS垃圾收集器.

G1收集器

G1垃圾收集器设计的初衷是为了尽量缩短处理超大堆(大于4G)时产生的停顿,G1收集器将堆分为多个区域(Region),不过它仍属于分代收集器,在进行Minor GC时,暂停所有的应用线程,采用多线程方式回收垃圾,将存活对象移动到老年代或者Survivor空间.

G1垃圾收集器属于Concurrent收集器:老年代的垃圾收集由后台线程完成,大多数工作不需要停止应用线程.由于老年代被分为多个区域,G1收集器通过将对象从一个区域复制到另一个区域,完成对象清理,同时也完成了堆的压缩整理.因此,G1收集器的堆不太容易发生碎片化.

同CMS收集器一样,避免Full GC的代价是消耗额外的CPU,负责垃圾收集的多个后台线程必须在应用线程运行的同时获得足够多的CPU运行周期,通过标志-XX:+UseG1GC启用

触发及禁用显示的垃圾回收

通常情况下垃圾收集是由JVM在需要的时候触发:新生代快用尽时会触发Minor GC,老年代快用尽的时候会触发Full GC,或者堆空间即将填满时会触发Concurrent垃圾收集.

JVM提供一种机制让应用程序强制进行GC.

  • System.gc()
  • jcmd GC.run
  • RMI作为分布式垃圾收集器的一部分,每隔一个小时它会调用System.gc()一次,可以通过-Dsum.rmi.dgc.server.gcInterval=N和-Dsum.rmi.dgc.client.gcInterval=N进行修改,N的单位为毫秒.Java 7中N默认为3600000(1小时).
    可以通过配置JVM参数-XX:+DisableExplicitGC显式地禁用这种类型的GC

小结

  1. 四种垃圾收集器分别采用了不同的方法来缓解GC对应用的影响.
  2. Serial收集器常用于仅有单CPU可用以及当其他程序会干扰GC的情况.
  3. Throughput收集器能够在其他的虚拟机上是默认值,它能最大化应用程序的总吞吐量,但有些操作会遭遇较长的停顿.
  4. CMS收集器能够在应用线程运行的同时并发地对老年代的垃圾进行收集,.如果CPU的计算能力足够支持后台垃圾收集线程的运行,该算法能够有效避免应用程序发生Full GC.
  5. G1收集器也能在应用线程运行的同时并发地对老年代的垃圾进行收集,在某种程度上能够减少发生Full GC的风险.G1的设计理念使得它比CMS更不容易遭遇Full GC.

选择GC算法

Serial收集器适用于应用程序的内存小于100MB的场景.这种情况下应用程序只需要很小的堆,无论是Throughput收集器的并行收集,还是CMS和G1收集器的后台收集都发挥不了太大的作用.

GC算法及批量任务

对于批处理任务而言,Throughput收集器所引入的停顿,尤其是Full GC停顿是主要的顾虑.如果有额外的CPU处理能力,那么Concurrent收集器将极大提高应用程序的性能.关键在于我们是否能够提供足够的CPU给Concurrent收集器的线程进行.
备注:使用Throughput收集器处理应用程序线程的批量任务能最大程度的利用CPU的处理能力,通常能获得更好的性能;如果批处理任务并没有使用机器上的所有可用CPU资源,那么切换到Concurrent收集器往往能获得更好的性能.

小结

  1. 衡量标准是响应时间或者吞吐量,在Throughput收集器和Concurrent收集器之间做选择的依据主要是有多少空闲的CPU资源能够用于运行后台的并发线程.
  2. 通常情况下,Throughput收集器的平均响应时间比Concurrent收集器要差,但是在90%的响应时间或者99%的响应时间这几项指标上,Throughput收集器比Concurrent收集器要好些.
  3. 使用Throughput收集器会超负荷地进行大量的Full GC,切换到Concurrent收集器通常会获得更低的响应时间.
  4. 选择Concurrent收集器时,如果堆较小,推荐使用CMS收集器.
  5. G1的设计使得它能够在不同的分区处理堆,因此它的扩展性更好,比CMS更容易于处理超大堆的情况.