Java垃圾收集器(GC)简介与最佳组合探究

Java经过近20年的演变,已经发展出一套复杂、健壮和高性能的垃圾收集器。在不同的应用场合下使用不同的GC组合能让程序性能得到可观提高。我想这也是Java这么多年来一直处于不败之地的原因之一。

以下讨论只限于Server模式下的HotSpot JVM。

GC的类型

Sun/Oracle的HotSpot JVM为我们提供了多种不同的GC,一种GC只专门负责新生代或老年代的内存回收工作,所以实际使用的时候需要我们为新生代和老年代指定不同的GC。但G1例外,因为G1可以通吃整个堆内存。

Serial GC

Serial GC是最基本也是年纪最大的GC,它由Sun随第一版本的Java一同发布。 Serial是一个单线程的用于新生代的 GC,因此它在工作的只有一个线程来完成GC,同时还必须让JVM 暂停执行所有用户线程,即有名的Stop The World。 这就意味着程序会有较长时间的停顿。所以对于服务端应用来说,Serial GC基本无用武之地,JVM默认也不会在新生代选用此GC。

ParNew GC

ParNew GC基本上是Serial GC的多线程版本,即在新生代的GC过程中会有多个线程线程同时执行清理,其它与Serial无异。因为是多线程,所以在多CPU环境下,它的性能会比Serial强一些。但如果是单CPU环境,它会带来线程上下文切换的时间开销,性能反而会不如Serial。ParNew是Server模式下的JVM的默认新生代收集器

Parallel Scavenge GC

该收集器也是一个作用于新生代的并行的多线程收集器,它与ParNew最大的区别在于它更加关注吞吐量(吞吐量 = 运行用户代码的时间 / (运行用户代码时间 + GC执行的时间) )。为了达到此目的,该GC提供了-XX:MaxGCPauseMillis参数和-XX:GCTimeRatio来控制吞吐量。前者是一个大于0的毫秒值,GC会尽可能保证垃圾收集耗时不超过该值。后者是一个大于0小于100的整数,意为垃圾收集耗时占总运行时间的比例。一般情况下,如果你的程序不是特别需要对GC吞吐量进行优化的话也不会手动指定使用该GC。

Serial Old GC 和 Parallel Old GC

在名称后面加上old意为该GC是为老年代服务的。前者在老年代回收时与前面提到的Serial相同,即单线程,会造成较长的停顿。后者相当于多线程版本,工作时也会先暂所有用户线程,然后仅仅是会启动多个线程执行GC而已。

CMS GC

CMS(Concurrent Mark Sweep)是一种以尽可能减少回收停顿时间为目标的收集器,只能作用于老年代。前面的的GC在执行回收算法时,必须先将挂起所有用户线程,待GC完成后用户线程才得以继续执行。CMS与之最大的区别是,CMS的回收过程可以部分并发地与用户线程同时执行。 CMS的回收分为以下几个阶段:

  1. 初始标记
  2. 并发标记
  3. 重新标记
  4. 并发清除

其中,最耗时的步骤2和4是并发执行的,即用户线程不需要停顿,回收可以与用户线程同时执行,这样就能大大缩短Stop The World的时间。但这并不意味着CMS就是个非常完美的GC了,它还有以下几个主要缺点:

  • CMS会与用户线程抢夺CPU资源。因为CMS的主要过程会与用户线程并发执行,因此用户线程此时执行速度会有一定的下降。如果是在单CPU情况下,这种情况会更加严重。
  • CMS可能会出现Concurrent Mode Failure错误。因为CMS在执行并发标记时,用户线程也在执行,因此在标记的同时还会有新垃圾在不断产生,这部分垃圾称为浮动垃圾,这是CMS无法进行回收处理的,只能等待下一次GC再说。也正是由于GC时用户线程还会执行,CMS必须在开始GC之前为用户线程预留一部分内存空间以供使用,而这是有风险的。这部分空间不够用户线程使用, 就会导致Concurrent Mode Failure, JVM会被迫启用Serial Old触发一次Full GC, 而执行一次Full GC的耗时是比较长的。

PS:
- Minor GC: 指的是对新生代进行垃圾回收的过程,这个过程一般会非常迅速。
- Full GC: 指对老年代执行的GC过程,在执行Full GC之前也可能会先执行一次Minor GC。Full GC通常会比Minor GC慢很多。

G1收集器

G1是目前垃圾收集技术发展的最新成果之一,它与前面的几款GC最大的不同在于:

  • G1可管理整个堆区,包括新生代和老年代。
  • G1在物理上不区分新生代和老年代。G1会把整个堆划分为很多区域(Region),新生代和老年代现在变更了仅仅是逻辑上的概念,它们并不需要在物理上严格区分。
  • G1会对所有Region进行回收效率排序,优先清理回收效率最高的Region。

除此之外,G1与CMS也是并发执行的GC,即执行清理时可以与用户线程同时(并发)执行,但是G1可以做到比CMS更短暂的停顿时间。

GC的选择

  • 对于服务端应用,我个人认为应当优先使用G1。除了G1的很多优秀特性以外,还有一个很重要的原因是Oracle打算在未来能用G1取代前面所有的GC,也就是说Oracle在今后肯定会把对GC优化的主要精力放在G1上。但是Oracle还是偏向于保守,因为直到今天的Java8,如果你不指定GC的话,JVM依然是使用ParNew + Serial Old的GC组合。不过我们可以通过
-XX:UseG1GC

参数命令JVM使用G1。

  • 对于客户端应用,如果不加任何参数的话,JVM会选择Serial + Serial Old组合,很不给力。最好添加:
-XX:+UseConcMarkSweepGC

即命令JVM在老年代使用CMS,以提高GC性能。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。