《java性能优化权威指南》---- 第7章:Java性能调优入门(7.8 调优延迟/响应性)

八、调优延迟/响应性

这一步调优的目标是达到程序的延迟性需求,包括多个活动的迭代:优化Java堆大小的配置、评估GC的持续时间和频率、是否可能切换到不同的垃圾收集器以及发生垃圾收集器切换之后进一步的内存调优。

评估垃圾收集器对延迟性影响的过程中将进行下面的活动:

  • 测量Minor GC的持续时间;
  • 统计Minor GC的次数;
  • 测量Full GC的最差(最长)持续时间;
  • 统计最差情况下, Full GC的频率。

测量GC的持续时间及频率对优化Java堆的大小至关重要。Minor GC的持续时间及频率决定了优化后新生代的大小。最差情况下的Full GC持续时间及频率决定了老年代的大小以及垃圾收集器的切换:是否需要从Throughput收集器(通过-XX:+UseParallelOldGC-XX:UseParallelGC选项启用)转向CMS收集器(通过-XX:+UseConcMarkSweepGC-XX:+UseParNewGC选项启用),如果Throughput收集器的Full GC的最差垃圾收集持续时间和频率远远不能满足应用程序的延迟性要求,那么就应该考虑切换到CMS,一旦发生切换,同样也需要针对CMS进行调优,本节后面会介绍这部分内容。

1、输入

这一步调优有多个输人,都源于应用程序的系统性需求。

  • 应用程序可接受的平均停滞时间。平均停滞时间将与测量出的Minor GC持续时间进行比较。
  • 可接受的Minor GC (会导致延迟)频率。Minor GC的频率将与可容忍的值进行比较。对应用程序干系人而言, GC持续的时间往往比GC发生的频率更重要
  • 可接受的应用程序的最大停顿时间。最大停顿时间将与最差情况下FullGC的持续时间进行比较口应用程序干系人可接受的最大停顿发生的频率。最大停顿发生的频率基本上就是Full GC的频率。同样,对于大多数应用程序干系人而言,相对于GC的频率,他们更关心GC持续的平均停顿时间和最大停顿时间。

一旦这些要求(输入)清楚以后,GC持续的时间和频率可以根据上一节介绍的方法使用Throughput收集器(通过-XX:+UseParallelOldGC-XX:UseParallelGC选项启用)收集。根据统计数据,不断调整新生代及老年代的大小,直到满足应用程序的要求。接下来的两小节将介绍如何根据Minor GC的持续时间及频率、最差情况下Full GC的持续时间及频率调整新生代、老年代的大小。

2、优化新生代大小

根据垃圾收集的统计数据、Minor GC的持续时间和频率可以确定新生代空间的大小。

分析GC数据时,如果发现Minor GC的间隔时间过长,修正的方法是减少新生代空间。如果Minor GC频率太高,修正的方法是增加新生代空间。

计算平均持续时间和频率时,Minor GC的次数越多,平均持续时间及频率的估计越准确。另外,使用应用程序运行于稳定阶段时的Minor GC值也是非常重要的。

我们看一个示例,图7-4的Minor GC使用了下面的HotSpot VM"命令行选项:
在这里插入图片描述
图7-4中,Minor GC平均持续时间是0.54秒。Minor GC的平均频率为每2.147秒一次。
在这里插入图片描述
真正达到应用程序的平均延迟要求之前可能要反复调整。调整新生代空间大小时,尽量保持老年代空间大小恒定。

使用图7-4中的垃圾收集数据,如果应用程序的延迟性要求是40毫秒,前面的示例中我们观察到平均Minor GC持续时间为54毫秒(0.054秒),大于应用程序的延迟要求。图7-4中用于生成数据的Java堆配置为-Xms6144m -Xmx6144m -Xm2048m -XX: PermSize=96m -XX:MaxPermSize=96m。这意味着老年代的大小为4096MB (老年代的大小通过-Xmx的值减去-Xmn的值计算得出),减少新生代大小10%的同时保持老年代大小不变,可以使用下面这条调整过的HotSpot VM命令行选项:
在这里插入图片描述
注意,-Xmn的值从2048m减小到了1844m,Java堆的大小(-Xmx-Xms )从6144m减少到了5940m。新生代空间(-Xmn )和Java堆的大小(-Xmx-Xms )都减少了204MB,约为之前新生代空间大小2048MB的10%。

调整新生代空间时,需要谨记下面几个准则。

  • 老年代空间大小不应该小于活跃数据大小的1.5倍。关于活跃数据大小的定义及老年代大小调整的准则请参考7.7节的内容。
  • 新生代空间至少应为Java堆大小的10%,通过-Xmx-Xms可以设定该值。新生代过小可能适得其反,会导致频繁的Minor GC。
  • 增大Java堆大小时,需要注意不要超过JVM可用的物理内存数。堆占用过多内存将导致底层系统交换到虚拟内存,反而会造成垃圾收集器和应用程序的性能低下。

这个阶段中,如果只考虑Minor GC引起的延迟,而调整新生代的大小又无法满足应用程序的平均停顿时间或延迟性要求,就只能修改应用程序或者改变JVM的部署模式,在多个JVM上部署应用程序,或者修改应用程序的平均延迟性要求。如果仅仅通过监控MinorGC就能达到应用程序的延迟性要求,你就可以直接进入到老年代空间的调整,调优应用程序的最差停滞时间和最差停滞频率。这是下一节要介绍的内容。

3、优化老年代大小

这一步的目标是评估Full GC引入的最差停滞时间以及Full GC的频率。实际中也应按平均最差停滞时间计算。如图7-5。
在这里插入图片描述
如果在GC日志中没有发现Full GC,可以参考7.7.4节去手动触发Full GC。另外,对Full GC频率的预估应该依据对象提升率进行计算,即对象从新生代复制到老年代空间的比率,接下来详细介绍如何计算提升率。

接下来是几个Minor GC的例子,用于说明如何计算Full GC的频率
在这里插入图片描述
从上面的GC日志,我们可以了解下面的内容:

  • Java堆的大小为6291 456KB或者6144MB (6 191 456 / 1024 );
  • 新生代大小为2 097 152KB或2048MB ( 2 097 152 / 1024 );
  • 老年代大小为6144MB-2048MB = 4096MB.

从老年代中减去活跃数据的大小(活跃数据的计算参看7.7.4节)可以得到可用老年代空间大小。这个例子中,假设活跃数据大小为1370MB,老年代大小为4096MB,活跃数据大小为1370MB。意味着老年代中有2726MB的空闲空间(4096-1370=2726)。

需要多长时间才能填满老年代中这2726MB的空闲空间取决于新生代到老年代的提升率提升率可以依据老年代空间占用的增长量以及每次Minor GC后新生代的空间占用计算得出

使用前面Minor GC的例子,每次Minor GC之后,老年代占用的空间分别为:
在这里插入图片描述
每次GC之后老年代的空间分别为:
在这里插入图片描述
每次Minor GC的平均提升为21 494KB,约为21MB。
除此之外,要计算提升率,我们还需要知道Minor GC的频率。前面的GC示例中,平均MinorGC的频率是每隔2.147秒一次。因此,提升率为21 494KB/2.147秒,或10 011KB (10MB )秒。填充满2726M可用老年代空间的时间约为272.6秒( 2726/10 = 272.6),大约是4.5分钟。

因此,根据前面的GC示例分析,该应用程序可以预期的最差Full GC频率是每4.5分钟一次。将应用程序运行于稳定态4.5分钟以上,观察Full GC的情况,可以很容易地验证这个预测。

如果预期或观测到Full GC的频率已经远远不能达到应用程序的最差Full GC频率要求,就应该增大老年代空间的大小。这个方法可以帮助降低Full GC的频率增加老年代空间的大小时注意保持新生代空间大小恒定。

如果你发现日志中只有Full GC
如果修改老年代空间大小后,只观察到Full GC,很可能是老年代与新生代空间大小失去了平衡导致应用程序只进行Full GC。这一情况通常缘于即使经过Full GC,老年代空间仍不足以容纳所有从新生代提升的对象。通过GC统计日志中的以下信息可以确认这种问题:
在这里插入图片描述
标识老年代空间不够大的一个线索是每次Full GC后,老年代中几乎没有任何空间被回收(ParOldGen标识右边的值)于此同时,新生代中总有大量的对象占用空间。当老年代中空间无法接纳从新生代中提升的对象时,正如我们在上面的输出中观察到的,这些对象会被“退还”(BackUp )到新生代空间中。

如果通过老年代空间大小调整的几次迭代之后,能满足应用程序的最差延迟性要求,JVM自身的调优步骤就已完成。你可以继续进入到调优过程的下一步“应用程序吞吐量调优”,这是下一节(7.9节)的主要内容。

如果由于Full GC持续时间过长,无法达到应用程序的最差延迟性要求,可以改用并行垃圾处理器。通过下面的HotSpot命令行选项可以开启CMS:

-XX:+UseConcMarkSweepGC

4、为CMS调优延迟

使用CMS收集器时,老年代垃圾收集线程与应用程序线程能实现最大的并行度。这为我们同时降低最差延迟出现的频率以及最差延迟的持续时间,避免发生长时间的GC提供了机会。CMS并不进行压缩,所以这一效果主要是通过避免老年代空间发生Stop-The-World压缩式垃圾来收集实现的。一旦老年代溢出就会触发Stop-The-World压缩式垃圾收集。

Stop-The-World这样的压缩式GC与Full GC之间存在着微妙的区别。在CMS中,如果老年代没有足够的空间处理来自新生代空间的对象晋升,只会在老年代空间触发一次Stop-The-World的压缩式GC。发生Full GC时,除非使用了-xx:-ScavengeBeforeFullGC选项,否则老年代和新生代的空间都会进行垃圾收集。

调优CMS收集器的目的是避免发生Stop-The-World的压缩式GC。与其他HotSpot VM垃圾收集器比较起来。 CMS收集器需要更细粒度的调优,尤其是对新生代空间大小进行更细致地调整,以及在需要时对何时启动老年代并行垃圾收集周期进行调整。

使用CMS时,如果老年代空间用尽,就会触发一个单线程Stop-The-World压缩式的垃圾收集。老年代空间耗尽并因此触发Stop-The-World压缩式垃圾收集时,由于应用程序长时间无法响应,会引起应用程序干系人的关注。因此,尽量避免用尽老年代空间是非常重要的。从Throughput收集器迁移到CMS收集器时需要遵守的一个通用原则是,将老年代空间增大20%-30%,这样才能更有效地运行CMS收集器。

回收操作会在老年代的可达对象之间形成空洞,从而引起可用空间的碎片化。有多种方法都可以解决碎片化问题,如下:

  • 通过Stop-The-World压缩式GC对老年代空间进行压缩,但是Stop-The-World压缩式GC停顿时间太长,所以这个方法不能从根本上解决碎片化问题。
    应用程序生命周期中努力达到的一个目标是,让老年代空间大到足以避免由堆内存碎片引起的Stop-The-World压缩。换句话说,就是“为GC申请最大内存原则”。
  • 减少对象从新生代提升至老年代的比率,即“Minor GC回收原则”。

晋升阈值控制新生代中的对象何时提升至老年代,它是根据Survivor空间占用的大小内部计算的结果。接下来介绍Survivor空间,随后会讨论晋升阈值。

5、Survivor空间介绍

在这里插入图片描述

跟CMS收集器不同,Throughput收集器默认就开启了一个名为"自适应大小调整)"(AdaptiveSizing)的功能,能够自动地调整Eden空间和Survivor空间的大小。但通用的操作,譬如对象如何分配,如何从Eden空间复制到Survivor空间,如何在Survivor空间之间复制,跟CMS收集器里的行为保持一致。

在所有的HotSpot垃圾收集器中,新生代空间都被划分成了一个Eden空间和2个Survivor空间。2块Survivor空间中,一块标记为"From" Survivor空间,另一块空间标记为"To" Survivor空间。Survivor空间的角色和它们的标识非常直观明了。Eden空间是分配新Java对象的空间。

如果Minor GC时, “To” Survivor空间不足以容纳所有从Eden空间和"From" Survivor空间中复制过来的活跃对象,超出的部分会提升至老年代空间。溢出至老年代空间会导致非计划的老年代空间消耗加速,最终导致Stop-The-World压缩式Full GC。再次提醒,针对Java应用程序的低延迟性要求进行调优时,我们要尽量避免Stop-The-World压缩式Full GC,换句话说,尽量遵守MinorGC回收原则。

**调整Survivor空间的大小,让其有足够的空间容纳存活对象足够长的时间,直到几个周期之后对象老化,就能避免发生Survivor空间溢出。有效的老化方法可以使老年代中只保存长期活跃的对象。**老化是保持对象在新生代中直到它们变得不可达的一种方法,这样做的目的是将老年代空间保留下来用于保存长期活跃的对象。

Survivor空间的大小可以通过HotSpot的命令行选项调整:

-XX:SurvivorRatio=<ratiox>

<ratio>的值必须大于0, -XX:SurvivorRatio=<ratiox>表示单个Survivor空间同Eden空间的大小的比率。下面的等式可以用于计算Survivor空间的大小:

survivor空间的大小=-Xmn<value>/(-XX: SurvivorRatio=<ratio> + 2)

等式中加2的原因是有两个Survivor空间。两个Survivor空间中的每一个大小均为512/(6+2)-64MB,剩下的384MB作为Eden空间。

对于给定的新生代,减少Survivor的比率会增大Survivor空间,同时减小Eden空间。同样,增大Survivor比率会减少Survivor空间,增大Eden空间。意识到减少Eden空间会导致更频繁的MinorGC是非常重要的。与之相反,增大Eden空间可以减少Minor GC的频率。同样非常重要的一点是,垃圾收集发生的频率越高,对象老化的速度就越快。

为了对Survivor空间大小做更细致的调整,优化新生代堆的大小,需要监控晋升阈值。晋升阈值决定了对象在新生代Survivor空间中保留的时间。如何监控晋升阈值以及如何根据监控的情况调整Survivor空间将在下面的内容中介绍。

6、解析晋升阈值

晋升阈值就是对象的年龄。一个对象的年龄就是它所经历的MinorGC次数。对象首次分配时,它的年龄为0。下一次Minor GC之后,如果该对象还在新生代,其年龄变为1。如果它在第二次Minor GC之后又存活下来,它的年龄变为2,以此类推。新生代空间中年龄大于HotSpot VMit算出的晋升阈值的对象都会被提升到老年代空间,换句话说,晋升阈值决定了对象在新生代中保持(或老化)的时间。

新生代中的有效对象老化可以避免将不成熟的对象提升到老年代空间,减少了老年代空间的占用率增长。同时,它还降低了CMS垃圾收集的执行频率,同时也减少了可能的空间碎片。

CMS使用的新生代垃圾收集器(称为ParNew收集器)会计算晋升阈值。同时,你可以使用HotSpot VM的命令行选项-XX:MaxTenuringThreshold=<n>指定。 HotSpot VM在对象的年龄超过<n>值时将其提升到老年代空间。内部计算出的晋升阈值不会超过最大晋升阈值。 Java 5 Update 6之后最大晋升阈值可以设置在0到15之间。

当目标Survivor空间的占用等于或小于HotSpot VM期望维护的值时,HotSpot VM将使用最大晋升阈值作为其计算出的晋升阈值。如果HotSpot VM认为它无法维持Survivor空间的占用,它会使用一个低于最大值的晋升阈值来保证目标Survivor空间的占用。比晋升阈值年龄大的对象都会被提升到老年代,换句话说,当存活下来的对象占用的空间超过目标Survivor空间的容量时就会发生溢出。

发生溢出的情况下,需要提升哪些对象,应该根据其实际年龄与晋升阈值进行比较。超过晋升阈值的对象才可以提升进入老年代。因此,监控晋升阈值对避免Survivor空间溢出是件非常重要的任务,而这将是下一节的主题。

7、监控晋升阈值

最大晋升阈值(-XX:MaxTenuringThreshold=<n>)请不要将它与内部计算出的晋升阈值相混淆。

-XX:+PrintTenuringDistribution

可以监控晋升的分布或者对象年龄分布,并以此为依据确定最优的最大晋升阈值值。

-XX:+PrintTenuringDistribution会输出每次Minor GC时晋升分布的情况。它也可以和其他的垃圾收集命令行选项,例如-XX:+PrintGCDateStamps,-XX:+PrintGCTimeStamps-XX:+PrintGCDetails配合使用。对Survivor空间的有效对象老化进行微调时,应该使用选项-XX:PrintTenuringDistribution在垃圾收集日志中包含晋升分布的统计信息。同样,如果需要在生产环境中判断一个应用程序事件是否源于一次Stop-The-World压缩式垃圾收集,往往也需要获取晋升分布的日志信息,使用该选项是非常有帮助的。

下面是使用-XX:PrintTenuringDistribution输出的一个例子:
在这里插入图片描述
这个例子中,最大晋升阈值设置为15,由(max 15)标识。通过new threshold 1可以知道虚拟机内部计算出的晋升阈值为1. Desired survivor size 8388608 bytes是Survivor空间的大小乘以目标存活率得到的空间大小。目标存活率是HotSpot VM预计目标空间在Survivor空间中占用的百分比。本章后续的内容将针对如何设置期望的Survivor空间大小进行更深入的介绍。

标题信息之下是对象年龄的列表。每个年龄的对象及其占用的空间大小单独列为一行,本例中,年龄为1的对象大小为16 690480字节。同时,在每一行中也会列出对象总的大小(字节数)。如果出现多年龄行的情况,总大小是该年龄行及其之前所有行对象大小的累计之和。后面的例子(8.8节)中有若干个年龄行的输出示例。

前文的示例中,期望Survivor空间大小(8388 608)远小于总的存活对象大小(16 690480 ),导致Survivor空间溢出,即最终Minor GC将一些对象提升到老年代。Survivor空间溢出表明Survivor空间过小。另外,由于最大晋升阈值为15,而HotSpot VM内部计算出的晋升阈值为1,这进一步验证了Survivor空间过小的问题。

通常情况下,观察到新的晋升阈值持续小于最大晋升阈值,或者观察到Survivor空间大小小于总的存活对象大小(即对象年龄最后最右列的值)都表明Survivor空间过小。

观察到Survivor空间过小时,要适当增大其容量。下面将介绍确定Survivor空间大小的流程。

8、调整Survivor空间的容量

调整Survivor空间容量的重要原则:调整Survivor空间容量时,如果新生代空间大小不变,增大Survivor空间会减少Eden空间;而减少Eden空间会增加Minor GC的频率。因此,为了同时满足应用程序Minor GC频率的要求,就需要增大当前新生代空间的大小;即增大Survivor空间大小时,Eden空间的大小应该保持不变。换句话说,每当Survivor空间增加时,新生代空间都应该增大。保持Eden空间大小恒定,Minor GC的频率就不会由于Survivor空间增大而发生变化。

通过-XX:+PrintTenuringDistribution选项输出中的所有对象年龄的总大小以及目标生存空间大小可以计算出应用程序需要的Survivor空间大小。

我们还是使用前面的例子来介绍:
在这里插入图片描述
存活对象的总大小是16 690 480字节。CMS默认情况下会使用大约50%的目标Survivor空间,根据这个原则, Survivor空间的大小应该设置为33 380 9607节,即16690 480/50%= 33 380 960字节。33 380 960字节约为32MB。为了更好地估算需要的Survivor空间,你应该让程序在稳定态运行一段时间,监控这段时间内的晋升分布,使用总的存活对象大小作为Survivor空间估算的更优值。

对于本例中的应用程序,为了更有效地老化对象,Survivor空间应该至少增大到32MB,假设前面示例中的晋升阈值输出是通过下面的HotSpot命令行生成的,那么Survivor空间的大小就是512/(30 + 2)=16MB:
在这里插入图片描述

  • 增大新生代空间
    同时,我们希望保持Minor GC的频率与之前一致,那么增大 Survivor空间到32MB需要更新HotSpot命令行,如下所示:在这里插入图片描述
    上例中新生代空间的大小增加了,Eden空间保持不变,Survivor空间增大。可以看到Java堆的大小(-Xmx和-Xms )以及新生代大小(-Xmn )都增大了32MB(16MB*2),另外,将两个Survivor空间的大小都设置为32MB,则-XX:SurvivorRatio=15 ( 544/(15 + 2)=32)。

  • 保持新生代空间不变
    如果实际情况限制不允许增大新生代容量,那么增大Survivor空间就只能以减少Eden空间为代价。下面是一个例子,该例中新生代空间保持不变,每个Survivor空间从16MB增大到32MB,同时Eden空间从480MB减小到448MB,即512/(14 +2)=32和512-32-32=448。
    在这里插入图片描述
    再次提醒,减少Eden空间大小会导致更频繁的Minor GC。但是与采用最初的方式分配的Java堆相比,由于增大了Survivor空间,对象在新生代保持的时间会更长。

假设保持Eden的大小不变,在修改过大小的堆上运行应用程序,即使用下面的HotSpot命令行选项:
在这里插入图片描述
产生的晋升分布如下:
在这里插入图片描述
输出的晋升分布中,位于最后一行最后一列的总存活对象大小7320 248字节小于期望的Survivor大小16 777 216字节,同时晋升阈值等于最大晋升年限,Survivor空间没有发生溢出,表明对象老化是有效的,没有发生Survivor溢出。

这个例子中,几乎没有对象的年龄超过3。你可能想要测试配置最大晋升年限为3的情况,即-XX: MaxTenuringThreshold=3,命令行选项如下所示:
在这里插入图片描述
这个配置与前一个配置的取舍在于后一个配置可以避免每次Minor GC时,“From” Survivor空间与"To" Survivor空间之间非必要的对象复制。如果你观察到垃圾收集中晋升分布与之前介绍的模式十分相似,即它**极少出现对象年龄为15的情况,并且也没有发生Survivor空间溢出,那么应该设置最大晋升阈值为其默认值15。**这种场景下,对象都不是长期存活对象,在年龄很小的时候就被回收了,根本不会生存到最大晋升年限的年龄15,他们在新生代空间中时就被Minor GC收集了,不会被提升到老年代空间。使用CMS时,任何提升到老年代空间并最终被垃圾收集的对象都会增加内存碎片,或者导致Stop-The-World压缩式垃圾收集。这些都不是我们所希望的。通常情况下,即使在Survivor空间之间多次复制对象也比匆匆将对象提升到老年代要好。

在Minor GC引起的应用程序延迟达标之前,你可能需要多次重复上面的步骤,监控晋升分布、修改Survivor空间或者重新配置新生代空间。

如果能够达到应用程序Minor GC的延迟性要求(持续时间、频率),你可以继续下一步,调优CMS垃圾收集周期的初始化。调优CMS垃圾收集周期的初始化将在后面进行介绍。

调优目标Survivor空间占用
目标Survivor空间占用是HotSpot VM尝试在Minor GC之后仍然维持的Survivor空间占用。通过HotSpot VM的命令行选项-XX:TargetSurvivorRatio=<percent>可以对该值进行调整。通过命令行选项指定的参数实际上是Survivor空间占用的百分比而不是一个比率。它的默认值是50。
HotSpot VM研发团队对不同类型的应用程序进行了大量的负荷测试,结果表明50%的目标Survivor空间占用能适应大多数的应用程序,这是因为它能应对Minor GC时存活对象的急速增加。

初始化CMS收集周期

一旦包含Eden空间和Survivor空间在内的新生代空间优化完成,Minor GC引入的延迟达到应用程序的要求之后,我们就可以把精力转向CMS收集器的调优上,减小最差情况的延迟并最小化最差延迟发生的频率。这一步的目标是维持空闲老年代空间的恒定,并由此避免发生Stop-The-World压缩式垃圾收集。

成功的CMS收集器调优要能以对象从新生代提升到老年代的同等速度对老年代中的对象进行垃圾收集。达不到这个标准则称之为“失速"( Lost the Race),失速的结果就会发生Stop-The-World压缩式垃圾收集。避免失速的关键是要结合足够大的老年代空间和足够快地初始化CMS垃圾收集周期,让它以比提升速率更快的速度回收空间。

如果CMS周期开始得太晚,就会发生失速。如果它无法以足够快的速度同收对象,就无法避免老年代空间用尽。但是CMS周期开始得过早,又会引起无用的消耗,影响应用程序的吞吐量。通常,早启动CMS周期要比晚启动CMS好,因为启动太晚的结果比启动过早的结果要恶劣得多。

如果你碰到了Stop-The-World压缩式垃圾收集,可以尝试调节CMS周期启动的时间。CMS中发生的Stop-The-World压缩式垃圾收集在垃圾收集日志中可以通过查找并发模式失效(ConcurrentMode Failure )定位。下面是一个示例:
在这里插入图片描述
输出字段中最重要的信息就是并发模式失效(concurrent mode failure)。如果你在垃圾收集日志中发现concurrent mode failures字样,可以通过下面的命令行选项通知HotSpot在更早的时间启动CMS垃圾收集周期。

-XX:CMSInitiatingOccupancyFraction=<percent>

设定的值是CMS垃圾收集周期在老年代空间占用达到多少百分比时启动。例如,如果你希望CMS周期在老年代空间占用达到65%时开始,可以设置-XX:CMSInitiatingOccupancyFraction=65

另一个可以与-XX:CMSInitiatingOccupancyFraction=<percent>一起使用的HotSpot命令行选项是

-XX:+UseCMSInitiatingOccupancyOnly

-XX:+UseCMSInitiatingOccupancyOnly告知HotSpot VM总是使用-XX:CMSInitiatingOccupancyFraction设定的值作为启动CMS周期的老年代空间占用國值,不使用-XX:+UseCMSInitiatingOccupancyOnly, HotSpot VM仅在启动的第一个CMS周期里使用-XX:CMSInitiatingOccupancyFraction设定的值作为占用比率,之后的周期中又转向自适应地启动CMS周期,即第一次CMS周期之后就不再使用-XX:CMSInitiatingOccupancyFraction设定的值。

通过选项设置何时启动CMS周期时,最好同时使用-XX:CMSInitiatingOccupancyFraction=<percent>-XX:+UseCMSInitiatingOccupancyOnly

-XX:CMSInitiatingOccupancyFraction设置的一个通用原则是老年代占用百分比应该至少应该是活跃数据大小的1.5倍。例如,按照下面使用的Java堆配置:
在这里插入图片描述
那么老年代的大小是1024MB ( 1536-512 = 1024),如果应用程序的活跃数据大小是350MB,那么应该在老年代空间占用达到约525MB,或空间百分比达到51%时(525/1024=51%)时启动CMS周期。这只是一个起点,后续还会根据监控的垃圾收集数据做进一步优化。下面是更新后的命令行,指定收集器在老年代空间占用达到51%时启动CMS周期:
在这里插入图片描述
通过垃圾收集的统计数据可以了解CMS周期是否启动得过早或过晚。

下面的例子中CMS周期启动得太晚(为了便于阅读,输出的内容调整成只显示垃圾收集的类型、堆占用情况及持续时间)。
在这里插入图片描述
请留意紧接在CMS-initial-mark之后的Full GC。 CMS-initial-mark是CMS周期的几个阶段之一。

下面是一个CMS周期启动过早的例子(为了便于阅读,输出的内容调整成只显示垃圾收集的类型、堆占用情况及持续时间)。
在这里插入图片描述

CMS周期以CMS-initial-mark标记开始,以CMS-concurrent-sweep和CMS-concurrentreset表示结束。我们可以看到首次CMS-initial-mark之后的堆占用是298 458KB。这之后,CMS-initial-mark和CMS-concurrent-reset之间的堆内存占用在ParNew Minor GC完成前后的变化很小。这个例子中CMS周期几乎没有回收任何垃圾数据。由于CMS-initial-mark表示的初始堆占用为298 458KB,同时Java堆大小为773 376KB,可以看出该例中设置JVM在老年代空间占用达到35%至40%之间时启动CMS周期(298 458KB/773 376KB =38.5%)。通过选项-XX:CMSInitiatingOccupancyFraction=50-XX:+UseCMSInitiatingOccupancyOnly可以强制在更高的堆占用时启动CMS周期比较合适。

下面是另一个示例,该示例中CMS周期回收了大量的老年代空间,但是没有发生Stop-The-World压缩式垃圾收集,即没有出现并发模式失效。

在这里插入图片描述

这个示例中,由CMs-initial-mark标记,表示CMS周期开始时老年代空间占用的值为548460KB。从CMS周期开始直到CMS-concurrent-reset标识CMS周期结束,ParNew Minor GC中老年代的使用显著减少。特别是在CMS-concurrent-sweep结束之前,有一个从561 33KB到368 910KB陡降。这表明有大约190MB的垃圾数据在CMS周期中被回收(561 336KB-368 910KB=192 426KB=187.91MB )。同时也请留意,首次CMS-concurrent-sweep的ParNew Minor GC之后的老年代占用为350 518KB,这一结果证实了在CMS周期中有超过190MB的垃圾数据被回收(561 336K-350 518K=210 818K=205.88MB)

如果你希望对CMS周期的启动进行细粒度的调优,请务必多尝试几个不同的老年代空间占用百分比。对垃圾收集日志进行监控和分析可以帮助你找到最适合你的应用程序的设置。

9、显式的垃圾收集

使用CMS时,如果你观察到由显式调用System.gc()触发的Full GC,有2种处理的方法。

  • (1)可以使用如下的HotSpot VM命令行选项,指定HotSpot VM以CMS垃圾收集周期的方式执行
    -XX:+ExplicitGCInvokesConcurrent #JDK6
    
    或者
    -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses #JDK6.14以上
    
  • (2)也可以使用下面的命令行通知HotSpot VM忽略显式的System.gc()调用:
    -XX:+DisableExplicitGC
    
    要留意的是,使用这个命令行选项也会导致其他HotSpot VM的垃圾收集器忽略显式的System.gc()调用。禁用显式的垃圾收集时应该慎重,它可能会对应用程序的性能造成较大影响。

如果在垃圾收集日志中发现了显式的Full GC,你需要先判断为什么它会发生,之后再决定是否要禁用,是否要把该调用从代码中移除,或者是否有必要指定一个条件来触发CMS并发垃圾收集周期。

10、并发永久代垃圾收集

下面的例子中的Full GC即源于永久代空间用尽:
在这里插入图片描述
永久代的空间的占用通过CMS Perm标识。可以看到, Full GC之前永久代的空间占用(65 534KB )与永久代的大小(655 36KB)相差无几,这表明该Full GC是由永久代空间用尽触发的。同时能看到,老年代空间离用尽还早,也没有CMS周期活跃的证据,综合所有这些线索可以断定发生了失速。

虽然永久代空间在垃圾收集日志中会以CMS Perm标记出,但是在CMS中, Hotspot VM默认情况下不会对永久代空间进行垃圾回收。通过下面的HotSpot VM命令行选项,你可以开启CMS的永久代垃圾收集

-XX:+CMSClassUnloadingEnabled

如果你使用的是Java 6 Update 3或更新的版本,也可以将下面的命令行选项与-XX:+CMSClassUnloadingEnabled一起使用:

-XX:+CMSPermGenSweepingEnabled

通过下面的选项可以控制在永久代空间占用百分比达到多少时启动CMS永久代垃圾收集

-XX:CMSInitiatinaPermOccupancyFraction=<percent>

选项代表的是启动CMS周期的永久代百分比。使用时,需要同时使用-XX:+CMSClassUnloadingEnabled选项。如果你希望将-xx:CMSInitiatingPermoccupancyFraction作为启动CMS周期的固定值必须使用下面的选项(类似前文中的:-XX:CMSInitiatingOccupancyFraction=<percent>-XX:+UseCMSInitiatingOccupancyOnly。**):

-XX:+UseCMSInitiatingOccupancyOnly

11、调优CMS停顿时间

CMS周期中有2个阶段是Stop-The-World的阶段,处于这2个阶段的应用程序线程会被阻塞。这两个阶段分别是初始标记阶段和重新标记阶段。虽然初始标记阶段是单线程的,却极少占很长的时间,通常情况下远小于其他的垃圾收集停顿。重新标记阶段是多线程的。

  • 通过下面的命令行选项可以控制重新标记阶段使用的线程数,建议将CMS收集线程数设置得小于默认值,否则由于大量的垃圾收集线程同时执行,应用程序的性能会受到极大的影响。

    -XX:ParallelGCThreads=<n>
    
  • 重新标记阶段的持续时间在某些时候可以通过下面的选项设置,该命令行选项强制HotSpot VM在进入CMS重新标记阶段之前先进行一次Minor GC,将重新标记阶段的工作量减到了最少。

    -XX:+CMSScavengeBeforeRemark
    
  • 如果应用程序有大量的引用对象或可终结对象要处理,使用下面的命令行选项可以减少垃圾收集的持续时间:

    -xx:+ParallelRefProcEnabled
    

12、下一步

完成这一步之后,就可以知道使用Throughput或CMS处理器能否达到应用程序的延迟性要求了。如果无法达到应用程序的延迟性要求,可以考虑7.11节中建议的性能选项。否则,就只能重新回顾应用程序的延迟性要求,对应用程序进行修改,可能还需要进行一些性能分析以定位出问题域;或者考虑其他JVM部署模式,将负荷分担到多个JVM实例上。如果应用程序可以满足延迟性要求,则可以继续进行下一步,即“应用程序吞吐量调优"。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值