欧阳亮的博客

编程不止是一份工作,还是一种乐趣!!!

JVM调优

工作这么多年,有过很多次通过JVM来分析和定位问题、调优生产服务器的性能的经历,却从来没有系统化的总结过JVM的调优过程。正好最近比较有些时间,系统化的总结一下多年积累下来的经验。

曾经有很多同事跟我提到过优化Java虚拟机,但是大部分人都认为在系统出现故障的时候,才需要去通过JVM来分析具体原因。事实上通过JVM分析和定位故障与调优完全是两回事,一般我们在谈到JVM调优的时候会涉及三个指标,分别是:内存占用量、系统延迟与系统吞吐量

  • 内存占用

    系统运行时,Java虚拟机需要的内存。

  • 延迟

    系统运行过程中由于垃圾收集引起的暂停时间。

  • 吞吐量

    单位时间内完成的任务数量。

明白了这三个指标,那JVM调优的目的也就显而易见了:追求更低的系统延迟和更高的系统吞吐量,衡量系统在稳定状态下所需要的最底内存占用量

值得注意的是,在优化的时候我们无法同时提升三项指标。一项指标的提升往往需要牺牲另外的一项或两项指标;换句话说,一项指标的妥协是为了提升其它一项或两项标指。好在大部分应用一般只观注其中一项或两项指标,比如大型网站的后台交易系统对系统的延迟非常敏感,又比如一些在夜间处理定时任务的应用,并不是很在意系统的延迟,但是对吞吐量的要求却很高,必须在给定的时间内完成一定数量的任务。

了解了JVM调优的目的之后,下一步是如何进行调优了。要进行JVM调优,我们首先需要收集来自Java虚拟机的各种信息,GC日志中包含大量有用的信息,第一步,我们需要读懂GC日志。

关于GC日志


在启动应用的时候加上参数-XX:+PrintGCDetails-Xloggc:${file},就可以生成GC日志。GC日志中每一行表示一次垃圾收集行为,其中包括垃圾收集开始的时间,垃圾收集发生的区域(轻年代young、老年代old还是永久代perm),垃圾收集前后的内存占用量、收集执行时间等信息。

下面的一段在Parallel Scavenge(轻年代收集器)加Parallel Old(老年代收集器)的环境中产生的GC日志:

15.078: [GC [PSYoungGen: 9044K->996K(9216K)] 46290K->38758K(50176K), 0.0006280 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
15.238: [GC [PSYoungGen: 9152K->996K(9216K)] 46914K->39258K(50176K), 0.0047185 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
15.376: [GC [PSYoungGen: 9166K->992K(9216K)] 47428K->40111K(50176K), 0.0011949 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
15.505: [GC [PSYoungGen: 9184K->1020K(9216K)] 48303K->40931K(50176K), 0.0019050 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
15.664: [GC [PSYoungGen: 9164K->996K(9216K)] 49076K->41684K(50176K), 0.0015982 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
15.666: [Full GC [PSYoungGen: 996K->0K(9216K)] [PSOldGen: 40687K->4137K(40960K)] 41684K->4137K(50176K) [PSPermGen: 9964K->9964K(20480K)], 0.0230596 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
15.863: [GC [PSYoungGen: 8128K->996K(9216K)] 12266K->5333K(50176K), 0.0007045 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

仅仅是一小段GC日志,但是却包含了大量丰富的信息,往往出乎你的意料,不信?

  1. 每一行的第一个数字,表示本次垃圾收集发生的时间。这是一个相对时间,表示距离VM启动到本次收集的秒数。(默认输出是距离JVM启动到本次垃圾收集的秒数,如果你希望输出具体时间,可以使用-XX:+PrintGCDateStamps参数)

  2. 后面紧跟着的[GC [PSYoungGen: ...]表示这是一次Minor GC,而[Full GC ...]则表示这是一次Full GC。

  3. 对于Minor GC,[PSYoungGen: 9044K->996K(9216K)]说明轻年代的总空间是9216K(注意,这里的总空间只是Eden区加上一个Survivor区的大小),在本次收集前已经使用了9044K空间,收集后存活对象还占用了996K的空间,本次收集回收了轻年代9044 - 996 = 8048K的空间。

  4. 紧跟在后面的46290K->38758K(50176K)说明堆空间(轻年代加上老年代的空间,不包括永久代)的总大小是50176K,在本次收集前已经使用了46290K的空间,收集后young代与old象的存活对象占用了38758K的空间,本次收集回收了堆空间46290 - 38758 = 7532K的空间。

  5. 堆空间的总大小减去轻年代的总空间 = 老年代的空间大小,即50176 - 9216 = 40960K。

  6. 轻代年回收的空间减去堆空间回收的空间,即可得出本次Minor GC发生之后,有多少对象从轻年代转移到了老年代,即对象从轻年代向老年代的转移量,8048 - 7532 = 516K,这是一个非常重要的信息。

  7. 结合多行Minor GC的发生时间,我们可以很容易的推算出Minor GC的发生频率;再分析每一次Minor GC发生时对象从轻年代向老年代的转移量,可以算出每次Minor GC时对象从轻年代向老年代的平均转移量。有了频率与平均转移量,再结合老年代的大小(注意:这里还需要除去老年代中长期存活的对象所占有的空间),我们就可以大致估计出系统大概多久会发生一次Full GC。

    例如,15.078至15.664这段时间内一共发生了5次Minor GC,(15.664 - 15.078) / 5 = 0.117,说明平均0.117秒就发生一次Minor GC。

    第一次的转移量:9044 - 996 - 46290 + 38758 = 516K

    第二次的转移量:9152 - 996 - 46914 + 39258 = 500K

    第三次的转移量:9166 - 992 - 47428 + 40111 = 857K

    第四次的转移量:9184 - 1020 - 48303 + 40931 = 792K

    第五次的转移量:9164 - 996 - 49076 + 41684 = 776K

    (516 + 500 + 857 + 792 + 776) / 5 = 688,平均每次Minor GC有688K的存活对象从轻年代向老年代转移,688 / 0.117 = 5880,即对象从轻年代向老年代的转移率是5880K/s。

    而老年代空间大小减去长期存活对象占用空间,再除以对象的转移率,(40960 - 4137) / 5880 = 6.26,即Full GC每隔6.26秒就会发生一次。当然,这只是一种估算的方法,越多的行参与,结果就越准确。

  8. 0.0006280 secs表示本次GC回收的耗时。

  9. Times: user=0.01 sys=0.00, real=0.00 secs提供了CPU的占用时间信息,一般不太观注。

  10. 对于Full GC,[PSOldGen: 40687K->4137K(40960K)]说明了old代的回收情况,这里需要重点注意4137K这个值,它表示老年代长期存活的对象所占用的空间,对我们后续优化JVM内存空间占用有很大的参考意义。[PSPermGen: 9964K->9964K(20480K)]表示永久代的回收情况,这里我们可以看到永久代的大小是20M。

对于追求低延迟的应用,可以使用ParNew加上CMS收集器的组合,如果是这样,那输出的GC日志会有些不同,但是所包含的信息量都是一样的。下面是一段使用ParNew收集器+CMS收集器所产生的GC日志:

11.743: [GC 11.743: [ParNew: 9121K->1008K(9216K), 0.0005529 secs] 28899K->20786K(50176K), 0.0006177 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
11.916: [GC 11.916: [ParNew: 9120K->1008K(9216K), 0.0006605 secs] 28899K->20986K(50176K), 0.0007215 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
12.092: [GC 12.092: [ParNew: 9103K->1008K(9216K), 0.0005308 secs] 29082K->20986K(50176K), 0.0005863 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
12.265: [GC 12.265: [ParNew: 9120K->1008K(9216K), 0.0009758 secs] 29099K->21388K(50176K), 0.0011337 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
12.446: [GC 12.446: [ParNew: 9122K->1008K(9216K), 0.0006966 secs] 29503K->21588K(50176K), 0.0007663 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
12.447: [GC [1 CMS-initial-mark: 20580K(40960K)] 21791K(50176K), 0.0003712 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
12.447: [CMS-concurrent-mark-start]
12.458: [CMS-concurrent-mark: 0.011/0.011 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
12.458: [CMS-concurrent-preclean-start]
12.458: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
12.458: [GC[YG occupancy: 1818 K (9216 K)]12.458: [Rescan (parallel) , 0.0001943 secs]12.458: [weak refs processing, 0.0000072 secs] [1 CMS-remark: 20580K(40960K)] 22399K(50176K), 0.0002792 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
12.459: [CMS-concurrent-sweep-start]
12.459: [CMS-concurrent-sweep: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
12.459: [CMS-concurrent-reset-start]
12.459: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
12.621: [GC 12.621: [ParNew: 9122K->1008K(9216K), 0.0004866 secs] 10388K->2573K(50176K), 0.0005400 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

其中包含[ParNew的行,表示这是一次Minor GC,其阅读方法与之前的介绍是一致的。从[GC [1 CMS-initial-mark:开始一直到[CMS-concurrent-reset: 0.000/0.000 secs]表示一次CMS回收周期的执行。CMS收集器的执行过程相对来说更复杂一些,整个过程分为初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)与并发清除(CMS concurrent sweep)4个步骤,GC日志的输出其实与这四个阶段也是对应的,后面在讲到具体优化的时候我们再详细介绍。

怎么样?短短几行的GC日志,包括了大量有用的信息,是否超出了您的预期?如果你能明白上面这些,那么恭喜你,你已经掌握了JVM调优的一把利器,下面我们具体看看如何优化内存占用量、系统延迟与系统吞吐量。

注意:在实际调优的时候,一定要在系统稳定运行时收集GC日志!!!

优化系统内存


优化内存的目的是尽可能是减少虚拟机的内存使用量,使得我们可以尽量以最小的硬件成本来支撑系统的运行。虽然在实际工作中我们更多的是把精力放在对性能的提升上,尤其现在的硬件成体相对于软件的维护成本来说仅仅只占一小部分,但是掌握内存优化的知识对性能的优化来说也是必不可少的。

在做内存优化之前,我们先了解一下Java虚拟机堆的结构。

Java VM堆的结构

Java虚拟机把堆内存划分为三个区域:轻年代、老年代与永久代:

  • 轻年代(young代)

    轻年代又分为一个Eden区和两个Survivor区(一个from Survivor和一个to Survivor),每次只会使用Eden和其中一个Survivor区,这么分配的原因是轻年代采用了“复制”算法来回收。当创建新的对象时,(大部分情况下)这个对象所占的空间会在Eden区中分配,如果Eden区的空闲空间不足,这时虚拟机会触发一次Minor GC,将Eden区和from Survivor区中还存活的对象转移到to Survivor区中。在经历若干次Minor GC之后,如果对象还是存活,那就会被移到老年代中去。

  • 老年代(old代)

    存放系统中长期存活的对象,比如通过spring拖管的一些单例对象,如service对象、dao对象等。还有一部分是在Minor GC后轻年代的空间仍然不足时,从轻年代转移过来的对象,这部分对象一般是导至系统发生Full GC的主要原因。

  • 永久代(perm代)

    存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

-Xmx-Xms这个两个参数分别指定虚拟机堆空间(轻年代加老年代)大小的最大值和最小值。当-Xms的值小于-Xmx的值的时候,Java堆的大小可以在最大值和最小值之前浮动。生产环境一般将这两个值的大小设成一致的,因为虚拟机动态调整Java堆大小的时候需要进行Full GC,影响系统的性能。

-Xmn参数用于指定轻年代的大小。虚拟机没有提供直接的参数来指定Eden区与Survivor的大小,而是通过指定Eden区与Survivor的比值-XX:SurvivorRatio来配置的。

永久代的大小是通过-XX:PermSize参数来配置的,例如:

-Xms50m -Xmx50m -Xmn10m -XX:PermSize=20m -XX:SurvivorRatio=8

-Xmx-Xms设置为50m,表示Java堆的总空间为50m,-Xmn10m表示轻年代空间大小为10m,即老年代空间大小为50 - 10 = 40m,-XX:PermSize=20m表示永久代空间大小为20m。-XX:SurvivorRatio=8表示Eden与Survivor的比例为8:1,不难算出Eden区的空间大小为8m,两个Survivor区的空间大小都为1m。

在启动Java应用的时候,上面提到的这些参数都不是必须的,这时虚拟机会根据应用的情况自行计算。而我们应该如何去衡量每个区域应该分配多少内存呢?在发生Full GC之后,老年代与永久代的存活对象所占有的空间,就是系统要求的最小空间。所以在系统运行稳定的时候收集Full GC日志可以估算出系统运行时长期存活对象所需要的空间大小,比如

15.666: [Full GC [PSYoungGen: 996K->0K(9216K)] [PSOldGen: 40687K->4137K(40960K)] 41684K->4137K(50176K) [PSPermGen: 9964K->9964K(20480K)], 0.0230596 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

老年代长期存活对象占用了4137K(约4M)的空间,永久代的存活对象占用了9964K(约10M)的空间。通常来讲,老年代在最小空间应该是存活对象大小的3到4倍,也就是12-16M之间,永久代的最小空间是存活对象的1.5倍左右,也就是10-15M之间,轻年代的大小应该配置为老年代存活对象的1到1.5倍,即4-6M之间。针对上面这条Full GC日志的分析,我们可以估算出最小的内存配置如下:

-Xms22m -Xmx22m -Xmn6m -XX:PermSize=15m -XX:SurvivorRatio=8

当然,内存占用量的优化,是以降低系统吞吐量和更高的延迟为代价的,在实际项目中几乎不会这么做,但是掌握内存优化的知识对其它两个指标的优化是很有帮助的。

系统延迟优化


对于一些提供基础服务的应用系统而言,对延迟的表现会非常的敏感,这样的系统建议使用ParNew加上CMS的收集器组合。CMS收集器是一种以获取最短回收停顿时间为目标的收集器,因为老年代的垃圾回收执行线程会和应用程序的线程并发的执行,这样就提供了一个机会来减少最坏延迟的频率和最坏延迟的时间消耗。

进行系统延迟优化的第一步,是通过GC日志、VM启动参数以及各种监控工具(如visualVM、jconsole等)采集信息,需要采集的信息包括:

  1. 虚拟机堆中各个区域的空间大小,轻年代(包括Eden区域与Survivor区域)、老年代与永久代,空间大小。

  2. 在发生Full GC之后,老年代与永久代的存活对象所占的空间大小。

  3. Minor GC的发生频率与执行耗时。

  4. CMS回收周期(也可以理解为Full GC吧)的触发频率及耗时,以及观察是否有发生Concurrent Mode Failure的情况。

  5. 对象从轻年代向老年代的转移率。

首先说明一下什么是Concurrent Mode Failure?CMS是一款并发的收集器,垃圾收集线程与用户线程是并发执行的,意味着垃圾收集线程在执行的同时,用户线程还可能会不断产生新的垃圾,称为“浮动垃圾”,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。如果CMS运行期间预留的内存被浮动垃圾占满而无法满足程序需要,就会出现一次Concurrent Mode Failure,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,也就是发生了stop-the-world的情况,这样停顿时间就很长了。对应延迟敏感的应用来讲Concurrent Mode Failure的情况经常出现的话,相当于系统故障了,必须立即解决。

为了避免或者说降低Concurrent Mode Failure的发生频率,虚拟机提供了一个参数-XX:CMSInitiatingOccupancyFraction,来指定一个百分比值,当老年代的空间使用率达到这个比值的时候,CMS就会触发一次回收周期。CMSInitiatingOccupancyFraction的值设置得过低会增加Full GC的发生频率,而设置过高又可能会导致Concurrent Mode Failure的频繁出现,在实际调优的时候需要不断的尝试不同的值来进行测试。另外,CMSInitiatingOccupancyFraction值的大小还依赖于对象从轻年代向老年代的转移率,如果转移率较高,那CMSInitiatingOccupancyFraction的值应该尽量设得低一些,如果转移率较低,就可以设得相对高一些。

另外需要注意的是参数-XX:+UseCMSInitiatingOccupancyOnly,它指定虚拟机总是使用-XX:CMSInitiatingOccupancyFraction的值作为老年代的空间使用率限制来启动CMS垃圾回收。如果没有使用-XX:+UseCMSInitiatingOccupancyOnly,那么虚拟机只是利用这个值来启动第一次CMS垃圾回收,后面都是使用自动计算出来的值。

对于延迟敏感的应用来说,Minor GC的执行耗时决定了应用的平均延迟,Full GC的耗时则决定了应用的最大延迟。要优化应用的最大延迟,我们需要从老年代入手。对于CMS收集器来说,我们可以认为它把老年代的空间划分成了三个部分:

CMS 老年代空间

其中长期存活对象指的是Full GC发生之后老年代中长期存活的对象所占用的空间,可用空间是用来存放从轻年代向老年代转移的对象的,保留空间是在CMS Full GC发生时,为了防止发生Concurrent Mode Failure而预留的一部分空间。不难发现,可用空间的大小结合对象从轻年代向老年代的转移率决定了老年代Full GC的发生频率,而可用空间的大小同时也决定了Full GC的执行耗时。很明显,要优化应用的最大延迟耗时,我们必须降低可用空间的大小,但是这样又会导致Full GC的频率变大,所以我们在降低可用空间的同时还得想办法降低对象从轻年代向老年代的转移率,尽可能让应用中的短期存活的对象在Minor GC阶段最大化的被回收。

虚拟机中轻年代的空间划分为一个Eden区域和两个Survivor区域(一个from,一个to):

轻年代空间

前面说过,当应用需要创建新的对象时,虚拟机就会为这个对象在Eden区中分配空间,如果Eden区的空间不足,虚拟机会触发一次Minor GC,将Eden区和from Survivor区中还存活的对象转移到to Survivor区中。不过也存在一些特殊情况:

  • 如果Survivor的空间不能容纳下存活对象,那虚拟机会根据对象的年龄有选择的将部分对象直接移动到老年代,这样就会加快Full GC发生的频率。

  • 虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制,但这样也会加剧Full GC发生的频率,良好的应用应该尽避免大对象的产生。

      注意 PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效。
    

对于轻年代来说,Eden区域在空间大小决定了Minor GC的发生频率,而Minor GC的耗时除了受到Eden与Survivor空间大小及比例之外,Minor GC之后存活对象大小也会有影响。但是在调优Minor GC的时候,我们通常把轻年代看作是一个整体,也就说是如果应用平均延迟的频率(即Minor GC发生的频率)高于预期,我们可以适当的增加轻年代的总空间;如果应用的平均延迟时间(即Minor GC的耗时)大于预期,我们就适当的减少轻年代的总空间。

应用的平均延迟时间与平均延尺频率是两个相互矛盾的因素,如果我们始终无法通过调整轻年代的大小来同时满足应用对这两个指标的要求,那:

  • 我们可能要修改应用程序,减少新对象的创建。

  • 或者改变虚拟机的部署模型。比如采用集群,把一个应用的压力分摊到多台机器上,这样应用对内存空间的要求就会降低,从而极大的提升平均延迟时间与平均延尺频率两个指标。

应用的平均延迟时间与平均延尺频率达到预期后,下一步就到了优化对象向老年代的转移率了。前面我们提到过对象的年龄,实际上虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

为了更好的适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半(target survivor space occupancy,默认值为50%,通过 -XX:TargetSurvivorRatio配置),年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。为了监控Survivor空间中对象的年龄分布情况,我们可以在启动虚拟机的时候加上参数-XX:+PrintTenuringDistribution

下面这个例子的启动参数为:

-Xms50m -Xmx50m -Xmn10m -XX:PermSize=20m -XX:SurvivorRatio=8

通过参数可以算出一个Survivor的空间大小正好是1M,其对应的GC日志如下:

5.013: [GC 5.013: [ParNew
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age   1:    1024800 bytes,    1024800 total
: 9121K->1008K(9216K), 0.0005689 secs] 17959K->10246K(50176K), 0.0006273 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

这个例子中,最大任期阀值被设置为15(通过max 15表示),但是内部计算出来的任期阀值是1,通过new threshold 1表示,说明Survivor的空间太小。Desired survivor size 524288 bytes的值等于一个Survivor的大小乘于0.5(target survivor space occupancy,默认值为50%,通过 -XX:TargetSurvivorRatio配置)。后面会列出对象的岁数列表。每行会列出每一个岁数的字节数,在这个例子中,岁数是1的对象有1024800字节,而且每行后面有一个总的字节数,如果有多行输出的话,总字节数是前面的每行的累加数。

由于期望的Survivor大小(524288)比实际总共Survivor字节数(1024800)小,也就是说,Survivor空间溢出了,这次MinorGC会有一些对象移动到老年代。另外,设定的最大任期阀值是15,但是实际上JVM使用的是1,也表明了Survivor的空间太小了。

要保证Survivor可以容纳下1024800的对象,那Survivor的空间大小必须大于1024800 / 0.5(target survivor space occupancy,默认值为50%,通过 -XX:TargetSurvivorRatio配置),约为2M。

所以我们增大Survivor的空间试试,注意在调整Survivor空间大小时,应该尽量保持其它区域的大小不变。所以我们把启动参数改为:

-Xms52m -Xmx52m -Xmn12m -XX:PermSize=20m -XX:SurvivorRatio=4

调整后的GC日志:

2.199: [GC 2.199: [ParNew
Desired survivor size 1048576 bytes, new threshold 1 (max 15)
- age   1:    1229544 bytes,    1229544 total
: 9521K->1208K(10240K), 0.0004707 secs] 9858K->1545K(51200K), 0.0005248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

通过调整后的GC日志发现,Survivor的空间还是小了,再用同样的方法得出Survivor的空间大小为4M,继续修改启动参数:

-Xms56m -Xmx56m -Xmn16m -XX:PermSize=20m -XX:SurvivorRatio=2

再次调整后的GC日志:

Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age   1:    1332016 bytes,    1332016 total
- age   9:        136 bytes,    1332152 total
- age  10:     332272 bytes,    1664424 total
: 9963K->1777K(12288K), 0.0008628 secs] 9963K->1777K(53248K), 0.0009226 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2.037: [GC 2.037: [ParNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age   1:    1229568 bytes,    1229568 total
- age  10:        136 bytes,    1229704 total
- age  11:     332288 bytes,    1561992 total
: 9890K->1685K(12288K), 0.0008882 secs] 9890K->1685K(53248K), 0.0009536 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2.205: [GC 2.205: [ParNew
Desired survivor size 2097152 bytes, new threshold 15 (max 15)
- age   1:    1229560 bytes,    1229560 total
- age  11:        136 bytes,    1229696 total
- age  12:     332328 bytes,    1562024 total
: 9798K->1687K(12288K), 0.0008638 secs] 9798K->1687K(53248K), 0.0009253 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

从任期分布的情况来看,Survivor空间没有溢出,由于存活的总大小没有超过预期的Survivor空间,以及任期阀值和最大任期阀值是相等的。这个表明,对象的老化速度是高效的,Survivor空间没有溢出。经过这一步的调整,我们已经极大的优化了对象向老年代的转移率了,但是重新调整轻年代的空间后,我们可能需要重新评估应用的平均延迟时间与平均延尺频率。

由于岁数超过1的对象很少,可以把最大任期阀值设置为1来测试一下,即设置选项-XX:MaxTenuringThreshhold=1。这样可以避免在MinorGC的时候不必要地把对象从“from” survivor复制到“to” survivor,从而加快Minor GC的执行时间,但也要注意CMS的对象从轻年代移动到老年代最终会导致碎片的增加,有可能导致stop-the-world压缩垃圾回收,这些都是不希望出现的,所以宁愿把-XX:MaxTenuringThreshhold尽量设大点,也不要太快的移动到老年代。

系统吞吐量优化


像一些在夜间处理定时任务的应用,并不是很在意系统的延迟,但是对吞吐量的要求却很高,这样的应用建议使用吞吐量优先的收集器,即Parallel Scavenge与Parallel Old的组合。

对于吞吐量的优化来说,Minor GC造成的影响一般可以忽略不计,主要是避免系统在稳定状态下发生Full GC,方法与低延迟优化的原理类似。一方面要保证老年代的可用空间大于长期存对象的1.5倍;另一方面,要优化每次Minor GC后对象从新年代向老年代的转移率。

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

但是关闭自适应大小以及优化Eden空间和Survivor空间以及老年代空间是探索提升应用吞吐量的一种办法。关闭自适应大小会改变应用的程序的灵活性,尤其是在修改应用程序,以及随着时间的推移应用的数据发生了变化。

使用参数-XX:-UseAdaptiveSizePolicy来关闭自适应调节策略,这时我们可以使用参数-XX:+PrintAdaptiveSizePolicy来输出关于Survivor空间占用更详细的信息,Survivor空间是否溢出,对象是否从新年代移动到老年代等信息。

下面这个例子的启动参数为:

-Xms50m -Xmx50m -Xmn10m -XX:PermSize=20m -XX:SurvivorRatio=8 -XX:+UseParallelGC
-XX:+PrintTenuringDistribution -XX:+PrintGCDetails -Xloggc:/Users/bobyes/gclog.txt
-XX:-UseAdaptiveSizePolicy  -XX:+PrintAdaptiveSizePolicy

对应的GC日志如下:

0.300: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1024112  promoted: 733296  overflow: true [PSYoungGen: 8112K->1000K(9216K)] 8112K->1716K(50176K), 0.0020283 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.466: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1044592  promoted: 766064  overflow: true [PSYoungGen: 9184K->1020K(9216K)] 9900K->2484K(50176K), 0.0009500 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
0.642: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1044592  promoted: 630880  overflow: true [PSYoungGen: 9197K->1020K(9216K)] 10661K->3100K(50176K), 0.0006310 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
0.814: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1015936  promoted: 618672  overflow: true [PSYoungGen: 9202K->992K(9216K)] 11283K->3676K(50176K), 0.0006963 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.983: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1015936  promoted: 749680  overflow: true [PSYoungGen: 9113K->992K(9216K)] 11797K->4408K(50176K), 0.0007983 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.157: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1015936  promoted: 643184  overflow: true [PSYoungGen: 9172K->992K(9216K)] 12589K->5036K(50176K), 0.0006074 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.336: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1015936  promoted: 622688  overflow: true [PSYoungGen: 9146K->992K(9216K)] 13190K->5644K(50176K), 0.0006587 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.518: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1015936  promoted: 307248  overflow: false [PSYoungGen: 9128K->992K(9216K)] 13781K->5944K(50176K), 0.0004796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.689: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1015936  promoted: 426064  overflow: true [PSYoungGen: 9116K->992K(9216K)] 14069K->6361K(50176K), 0.0005243 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.865: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1015936  promoted: 409664  overflow: false [PSYoungGen: 9109K->992K(9216K)] 14478K->6761K(50176K), 0.0006121 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]

以GCAdaptiveSizePolicy开头的一些额外信息输出来了,survived标签表明To Survivor空间的对象字节数,promoted标签表示转移到老年代的字节数,而overflow表示新年代是否有对象溢出到老年代,换句话说,就是表明了To Survivor是否有足够的空间来容纳从Eden空间和From Survivor空间移动而来的对象。为了更好的吞吐量,期望在应用处于稳定运行状态下,survivor空间不要溢出。如果Survivor空间溢出,对象会再达到任期阀值或者消亡之前被移动到老年代。换句话说,对象过快的移动到老年代。频繁的Survivor空间溢出会导致FullGC。

从上面的GC日志我们可以看出,每次Minor GC时To Survivor空间的存活对象大约是1M(通过survived标签判断),通过之前的介绍我们知道,如果Survivor空间的使用率超过了参数-XX:TargetSurvivorRatio的值,虚拟机会在对象达到最大岁数之前把对象移动到老年代。要优化这个应用,要求Survivor的空间大小 * TargetSurvivorRatio的值要大于1M,即Survivor的空间应该调整为2M。

优化后的启动参数如下(调整Survivor大小的同时,最好保持其它区域的大小不变):

-Xms52m -Xmx52m -Xmn12m -XX:PermSize=20m -XX:SurvivorRatio=4 -XX:+UseParallelGC
-XX:+PrintTenuringDistribution -XX:+PrintGCDetails -Xloggc:/Users/bobyes/gclog.txt
-XX:-UseAdaptiveSizePolicy  -XX:+PrintAdaptiveSizePolicy

调整后的GC日志:

1.507: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1827040  promoted: 0  overflow: false [PSYoungGen: 9820K->1784K(10240K)] 9820K->1784K(51200K), 0.0005199 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.683: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1638608  promoted: 0  overflow: false [PSYoungGen: 9909K->1600K(10240K)] 9909K->1600K(51200K), 0.0007394 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.855: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1757424  promoted: 0  overflow: false [PSYoungGen: 9717K->1716K(10240K)] 9717K->1716K(51200K), 0.0004394 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2.030: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1843440  promoted: 0  overflow: false [PSYoungGen: 9829K->1800K(10240K)] 9829K->1800K(51200K), 0.0005383 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
2.213: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1384640  promoted: 0  overflow: false [PSYoungGen: 9913K->1352K(10240K)] 9913K->1352K(51200K), 0.0008055 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2.390: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1757392  promoted: 0  overflow: false [PSYoungGen: 9465K->1716K(10240K)] 9465K->1716K(51200K), 0.0005255 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2.568: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1654976  promoted: 0  overflow: false [PSYoungGen: 9831K->1616K(10240K)] 9831K->1616K(51200K), 0.0005804 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2.741: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1741024  promoted: 0  overflow: false [PSYoungGen: 9730K->1700K(10240K)] 9730K->1700K(51200K), 0.0006533 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2.907: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1532128  promoted: 344176  overflow: false [PSYoungGen: 9813K->1496K(10240K)] 9813K->1832K(51200K), 0.0007186 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
3.088: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1155232  promoted: 8192  overflow: false [PSYoungGen: 9613K->1128K(10240K)] 9949K->1472K(51200K), 0.0002540 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
3.263: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1257648  promoted: 0  overflow: false [PSYoungGen: 9241K->1228K(10240K)] 9585K->1572K(51200K), 0.0002572 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
3.438: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1462480  promoted: 0  overflow: false [PSYoungGen: 9341K->1428K(10240K)] 9685K->1772K(51200K), 0.0003548 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
3.613: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1462480  promoted: 0  overflow: false [PSYoungGen: 9541K->1428K(10240K)] 9885K->1772K(51200K), 0.0002940 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
3.790: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1564896  promoted: 0  overflow: false [PSYoungGen: 9543K->1528K(10240K)] 9887K->1872K(51200K), 0.0003094 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
3.975: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1224880  promoted: 0  overflow: false [PSYoungGen: 9641K->1196K(10240K)] 9985K->1540K(51200K), 0.0003254 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
4.148: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:  survived: 1257648  promoted: 0  overflow: false [PSYoungGen: 9309K->1228K(10240K)] 9653K->1572K(51200K), 0.0002564 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

从优化后的GC日志可以看出,虽然偶尔会有对象转移到老年代,但是并不有发生溢出,说明对象老新年代向老年代的转率是高效的。后面我们可以直接调整老年代的空间大小来控制Full GC的频率和延迟时间,如果始终无法在频率和延迟时间之间找到合理的平衡,那就只能修改应用的部暑模型了。

Java 6 Update 23之后,默认的垃圾回收线程是通过Runtime.availableProcessors()获得的,如果这个方法的返回值小于等于8,那么就用这个返回值,如果比8更大,那么就取这个值的5/8。如果运行多个应用,可以根据应用的情况来分配线程数(通过参数-XX:ParallelGCThreads来指定),如果应用的消耗是相当的,那么就用CPU的内核数除以应用数得到每一个应用可以分配的线程。如果应用的load不相当,那么就可以根据应用的实际情况来衡量和分配。