1. 并行收集器
在多核时代,并行处理将会很大程度地提升程序性能。垃圾收集器也存在并行收集器,它们是在串行收集器的基础上改进后得到的。并行收集器使用多个线程同时进行垃圾回收,在多核CPU的硬件环境中可以有效地缩短垃圾回收所需要的时间。
1.1. ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为如所有控制参数(例如:-XX:SurvivorRatio
、-XX:PretenureSizeThreshold
、-XX:HandlePromotionFailure
等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew收集器的工作过程如下图所示:
- 它是许多运行在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收集器的工作过程下图所示:
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收集器要好:
接下来测试使用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]
得到的聚合报告如下:
可以发现其实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余次/秒:
再来试试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余次/秒:
可以发现,在堆内存较小的情况下,相较于ParNew收集器Parallel Old收集器有性能上的提升。