Java
Java虚拟机

Java虚拟机05 - 垃圾收集器之并行收集器

简介:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

1. 并行收集器

在多核时代,并行处理将会很大程度地提升程序性能。垃圾收集器也存在并行收集器,它们是在串行收集器的基础上改进后得到的。并行收集器使用多个线程同时进行垃圾回收,在多核CPU的硬件环境中可以有效地缩短垃圾回收所需要的时间。

1.1. ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为如所有控制参数(例如:-XX:SurvivorRatio-XX:PretenureSizeThreshold-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew收集器的工作过程如下图所示:

1.ParNew运行流程.png

  • 它是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为除了Serial收集器外,目前只有它能与CMS收集器配合工作。
  • 由于存在线程交互的开销,ParNew收集器在单CPU环境下性能并没有单线程垃圾收集器性能好。
  • 可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。一般来说,当CPU数量小于8个时,宜设置为CPU数量;当CPU数量大于8个时,宜设置为3 + (( 5 * CPU_Count ) / 8 )

开启ParNew收集器可以使用以下参数:

  • -XX:+UseParNewGC:新生代使用ParNew收集器,老年代使用串行收集器。
  • -XX:+UseConcMarkSweepGC:新生代使用ParNew收集器,老年代使用CMS收集器。

注:在谈论垃圾收集器的上下文语境中,并行和并发的解释如下:
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

1.2. Parallel Scavenge收集器

Parallel Scavenge收集器与ParNew收集器类似,也是使用复制算法的并行的多线程新生代收集器。但Parallel Scavenge收集器关注可控制的吞吐量(Throughput)。

注:吞吐量是指CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /( 运行用户代码时间 + 垃圾收集时间 )

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:

  • -XX:MaxGCPauseMillis:最大垃圾收集停顿时间,是一个大于0的毫秒数,收集器将回收时间尽量控制在这个设定值之内;但需要注意的是在同样的情况下,回收时间与回收次数是成反比的,回收时间越小,相应的回收次数就会增多。所以这个值并不是越小越好。
  • -XX:GCTimeRatio:吞吐量大小,是一个(0, 100)之间的整数,表示垃圾收集时间占总时间的比率。

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

1.3. Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的。

由于如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择,Parallel Old收集器的出现就是为了解决这个问题。Parallel Scavenge和Parallel Old收集器的组合更适用于注重吞吐量以及CPU资源敏感的场合。Parallel Old收集器的工作过程下图所示:

2.Parallel Old运行流程.png

Parallel Old也是更加关注吞吐量,当使用参数-XX:+UseParallelOldGC可以在新生代使用Parallel Scavenge收集器,而在老年代使用Parallel Old收集器,同时,使用参数-XX:ParallelGCThreads可以设置垃圾回收时的线程数量。

2. 并行收集器测试

下面我们将对比串行收集器,对并行收集器进行一些测试。这里沿用上一节中串行收集器最后的测试基准设置,相应的基准CATALINA_OPTS如下:

  • CATALINA_OPTS="-Xloggc:/Users/qinly/Desktop/Java_Test/gc.log -XX:+PrintGCDetails -Xms128M -Xmx512M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/qinly/Desktop/Java_Test/ -XX:PermSize=32M"

先使用ParNew收集器进行测试,设置CATALINA_OPTS如下:

  • CATALINA_OPTS="-Xloggc:/Users/qinly/Desktop/Java_Test/gc.log -XX:+PrintGCDetails -Xms128M -Xmx512M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/qinly/Desktop/Java_Test/ -XX:+UseParNewGC -XX:PermSize=32M"

可以获取相应的GC日志如下:

  • 1.074: [GC 1.074: [ParNew: 34944K->4352K(39296K), 0.0072404 secs] 34944K->5256K(126720K), 0.0073213 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
  • 1.212: [GC 1.212: [ParNew: 39296K->4352K(39296K), 0.0283684 secs] 40200K->31278K(126720K), 0.0284078 secs] [Times: user=0.10 sys=0.01, real=0.03 secs]
  • 1.270: [GC 1.270: [ParNew: 39296K->4352K(39296K), 0.0283448 secs] 66222K->58886K(126720K), 0.0283816 secs] [Times: user=0.09 sys=0.01, real=0.03 secs]
  • 2.393: [GC 2.393: [ParNew: 39296K->4352K(39296K), 0.0172435 secs] 93830K->74423K(126720K), 0.0172897 secs] [Times: user=0.06 sys=0.01, real=0.01 secs]
  • 13.612: [GC 13.612: [ParNew: 39296K->4352K(39296K), 0.0140403 secs] 109367K->85901K(126720K), 0.0141204 secs]

从请求的聚合报告显示情况可以看到,请求的情况比使用Serial收集器要好:

3.ParNew GC聚合报告.png

接下来测试使用Parallel Scavenge和Parallel Old收集器,这两个收集器只能搭配使用,一旦新生代使用了Parallel Scavenge,老年代只能使用Serial Old或Parallel Old。设置CATALINA_OPTS参数如下:

  • CATALINA_OPTS="-Xloggc:/Users/qinly/Desktop/Java_Test/gc.log -XX:+PrintGCDetails -Xms128M -Xmx512M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/qinly/Desktop/Java_Test/ -XX:+UseParallelOldGC -XX:ParallelGCThreads=4 -XX:PermSize=32M"

可以获取相应的GC日志如下:

  • 2.537: [GC [PSYoungGen: 32768K->3664K(38208K)] 32768K->3664K(125632K), 0.0039270 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
  • 2.713: [GC [PSYoungGen: 36432K->5416K(38208K)] 36432K->28740K(125632K), 0.0234003 secs] [Times: user=0.08 sys=0.01, real=0.03 secs]
  • 2.764: [GC [PSYoungGen: 38184K->5408K(38208K)] 61508K->54508K(125632K), 0.0249121 secs] [Times: user=0.08 sys=0.01, real=0.03 secs]
  • 2.813: [GC [PSYoungGen: 38176K->5424K(70976K)] 87276K->80484K(158400K), 0.0254232 secs] [Times: user=0.09 sys=0.02, real=0.03 secs]
  • 2.838: [Full GC [PSYoungGen: 5424K->0K(70976K)] [ParOldGen: 75060K->79067K(160960K)] 80484K->79067K(231936K) [PSPermGen: 15645K->15630K(32768K)], 0.3371749 secs] [Times: user=0.99 sys=0.00, real=0.33 secs]
  • 3.230: [GC [PSYoungGen: 65536K->5440K(70976K)] 144603K->130995K(231936K), 0.0441778 secs] [Times: user=0.15 sys=0.03, real=0.04 secs]
  • 3.274: [Full GC [PSYoungGen: 5440K->0K(70976K)] [ParOldGen: 125555K->127385K(249984K)] 130995K->127385K(320960K) [PSPermGen: 15667K->15667K(35520K)], 0.1846914 secs] [Times: user=0.60 sys=0.00, real=0.19 secs]
  • 3.512: [GC [PSYoungGen: 65536K->52096K(116480K)] 192921K->179481K(366464K), 0.0430427 secs] [Times: user=0.14 sys=0.03, real=0.04 secs]
  • 12.383: [GC [PSYoungGen: 110336K->58225K(116480K)] 237721K->206171K(366464K), 0.0656194 secs] [Times: user=0.20 sys=0.03, real=0.07 secs]
  • 16.616: [GC [PSYoungGen: 116465K->58225K(116480K)] 264411K->226083K(366464K), 0.0626982 secs]

得到的聚合报告如下:

4.Parallel Old GC聚合报告.png

可以发现其实Parallel Old相较于ParNew GC的改善并不十分明显。我们可以降低初始堆内存的大小,再对两者进行测试,首先是ParNew GC,相应的CATALINA_OPTS调整如下:

  • CATALINA_OPTS="-Xloggc:/Users/qinly/Desktop/Java_Test/gc.log -XX:+PrintGCDetails -Xms96M -Xmx512M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/qinly/Desktop/Java_Test/ -XX:+UseParNewGC -XX:PermSize=32M"

可以获取相应的GC日志如下:

  • 2.123: [GC 2.123: [ParNew: 26240K->2977K(29504K), 0.0040935 secs] 26240K->2977K(95040K), 0.0041779 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
  • 2.379: [GC 2.379: [ParNew: 29217K->3264K(29504K), 0.0174535 secs] 29217K->18394K(95040K), 0.0174906 secs] [Times: user=0.06 sys=0.01, real=0.02 secs]
  • 2.434: [GC 2.434: [ParNew: 29504K->3264K(29504K), 0.0234666 secs] 44634K->39137K(95040K), 0.0235027 secs] [Times: user=0.08 sys=0.01, real=0.02 secs]
  • 2.480: [GC 2.480: [ParNew: 29504K->3264K(29504K), 0.0208958 secs] 65377K->59105K(95040K), 0.0209410 secs] [Times: user=0.07 sys=0.01, real=0.03 secs]
  • 2.523: [GC 2.523: [ParNew: 29504K->3264K(29504K), 0.0228075 secs]2.546: [Tenured: 76673K->76737K(76864K), 0.1364475 secs] 85345K->79936K(106368K), [Perm : 15644K->15644K(32768K)], 0.1594976 secs] [Times: user=0.21 sys=0.01, real=0.16 secs]
  • 2.725: [GC 2.725: [ParNew: 51200K->6400K(57600K), 0.0500838 secs] 127937K->119441K(185496K), 0.0501381 secs] [Times: user=0.18 sys=0.01, real=0.05 secs]
  • 11.975: [GC 11.975: [ParNew: 57600K->6400K(57600K), 0.0247344 secs]12.000: [Tenured: 131702K->126706K(131736K), 0.2849788 secs] 170641K->126706K(189336K), [Perm : 18930K->18930K(32768K)], 0.3117178 secs]

得到的聚合报告如下,可以发现这次吞吐量降到了5000余次/秒:

5.降低初始堆大小后的ParNew GC聚合报告.png

再来试试Parallel Old收集器,相应的CATALINA_OPTS调整如下:

  • CATALINA_OPTS="-Xloggc:/Users/qinly/Desktop/Java_Test/gc.log -XX:+PrintGCDetails -Xms96M -Xmx512M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/qinly/Desktop/Java_Test/ -XX:+UseParallelOldGC -XX:ParallelGCThreads=4 -XX:PermSize=32M"

可以获取相应的GC日志如下:

  • 2.201: [GC [PSYoungGen: 24576K->2874K(28672K)] 24576K->2874K(94208K), 0.0030818 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
  • 2.526: [GC [PSYoungGen: 27450K->4076K(28672K)] 27450K->16116K(94208K), 0.0124124 secs] [Times: user=0.04 sys=0.01, real=0.01 secs]
  • 2.580: [GC [PSYoungGen: 28652K->4080K(28672K)] 40692K->35192K(94208K), 0.0173779 secs] [Times: user=0.06 sys=0.01, real=0.02 secs]
  • 2.617: [GC [PSYoungGen: 28656K->4080K(53248K)] 59768K->54016K(118784K), 0.0185125 secs] [Times: user=0.06 sys=0.01, real=0.01 secs]
  • 2.636: [Full GC [PSYoungGen: 4080K->0K(53248K)] [ParOldGen: 49936K->53386K(112576K)] 54016K->53386K(165824K) [PSPermGen: 15682K->15667K(32768K)], 0.1973456 secs] [Times: user=0.59 sys=0.00, real=0.20 secs]
  • 2.879: [GC [PSYoungGen: 49152K->4064K(53248K)] 102538K->93402K(165824K), 0.0352487 secs] [Times: user=0.10 sys=0.02, real=0.03 secs]
  • 2.914: [Full GC [PSYoungGen: 4064K->0K(53248K)] [ParOldGen: 89338K->91372K(184192K)] 93402K->91372K(237440K) [PSPermGen: 15667K->15667K(35328K)], 0.1753835 secs] [Times: user=0.52 sys=0.00, real=0.18 secs]
  • 3.127: [GC [PSYoungGen: 49152K->40416K(104768K)] 140524K->131788K(288960K), 0.0225998 secs] [Times: user=0.06 sys=0.02, real=0.02 secs]
  • 14.940: [GC [PSYoungGen: 104352K->52849K(116800K)] 195724K->156190K(300992K), 0.0389903 secs]

得到的聚合报告如下,吞吐量降到了6000余次/秒:

6.降低初始堆大小后的ParNew GC聚合报告.png

可以发现,在堆内存较小的情况下,相较于ParNew收集器Parallel Old收集器有性能上的提升。