JVM性能调优入门

1. 背景

虽然大多数应用程序使用JVM的默认设置就能很好地工作,仍然有不少应用程序需要对JVM进行额外的配置才能达到其期望的性能要求。

现在JVM为了满足各种应用的需要,为程序运行提供了大量的JVM配置选项。不幸的是,针对一个应用程序进行的JVM调优(配置)可能并不适用于另一个应用程序。

注意:为了达到应用程序的系统需求,可能需要进行多次迭代才能获得应用程序的性能要求。

2. 应用程序的系统需求

吞吐量、响应时间、内存消耗量、可用性、可管理性等。

2.1 可用性

可用性是对应用程序处于可操作,可使用状态的度量。

可用性需求指的是当引用程序的某些组件发生故障或失效时,应用程序或应用程序的一部分在多大程度上还可以继续提供服务。

2.2 可管理性

可管理性是对由运行,监控应用程序而产生的操作性开销的度量,同时也包含了配置应用程序的难易程度。

2.3 吞吐量

吞吐量是对单位时间内处理工作量的度量。

涉及吞吐量需求时,一般不考虑它对延迟或响应时间的影响。通常情况下,增加吞吐量的代价是延迟的增加或内存使用的增加。

2.4 延迟及响应性

延迟或响应性是对应用程序收到指令开始工作直到完成该工作消耗时间的度量。

定义延迟或响应性需求时并不考虑程序的吞吐量,通常情况下,提高响应性或缩小延迟的代价是更低的吞吐量或更多的内存消耗。

2.5 内存占用

内存占用指的同等程度的吞吐量,延迟,可用性和可管理性前提下,运行应用程序所需的内存大小。

内存占用通常以运行应用程序需要的Java堆大小或运行应用程序需要的总内存大小来表述。一般情况下,通过增大Java堆的方式增加可用内存能够提高吞吐量,降低延迟或兼顾两者。

2.6 启动时间

启动时间是应用程序初始化所消耗的时间。

3. JVM部署模式

JVM部署模式选择指的是将应用程序部署到单个JVM实例上,还是部署到多个JVM实例上。

3.1 单JVM部署模式

采用单JVM部署模式的应用程序存在单点故障的问题。

3.2 多JVM部署模式

将JVM应用程序部署到多个JVM实例能够获得更好的可用性,以及更低延迟的可能性。

多JVM部署模式可能提供更低的延迟,这是因为多JVM部署模式下,Java堆通常比较小,较小的堆在垃圾收集时产生的停顿更小。通常情况下,垃圾收集所产生的停顿是影响应用程序延迟性的最主要因素。

一般情况下,使用的JVM数目越少越好,监控和管理成本就越低,消耗的总内存也更少。

4. JVM运行模式

4.1 Client模式或Server模式

HotSpot VM有3种运行模式:Client模式、Server模式、Tiered Server模式。

Client模式的特点是启动快、占用内存少、JIT编译器生成代码的速度也更快。

Server模式则提供了更复杂的生成码优化功能,这个功能对于服务器应用而言尤其重要。大多数Server模式的JIT编译优化都要消耗额外的时间以收集更多的应用程序行为信息、为应用程序运行生成更优的生成码。

Tiered Server模式结合了Client和Server运行模式的长处,即快速启动和高效的生成码。通过-server -XX:+TieredCompilation命令行选项可以启动Tiered Server模式。

4.2 32位/64位JVM

除了Client模式和Server模式,JVM部署时还有另一个选项:32位JVM或64JVM。

注意:64位HotSpot虚拟机中没有提供Client模式。

对于使用-XX:+UseCompressedOops选项的64位HotSpot VM,最大堆小于等于26GB时性能最好。

Java 6 Update 18之后的HotSpot VM能够根据最大Java堆的情况自动启动-XX:+UseCompressedOops。

4.3 垃圾收集器

HotSpot VM提供了多个垃圾收集器:Serial收集器,Throughput(PS)收集器,Mostly-Concurrent(CMS)收集器及G1收集器等。具体参考【垃圾收集器】

5. 垃圾收集调优基础

介绍影响垃圾收集性能的三个主要的属性,垃圾收集调优的三个基本原则,HotSpot VM垃圾收集调优时需要采集的信息。

5.1 性能属性

吞吐量:不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标;

延迟:度量标准是缩短由于垃圾收集引起的停顿时间或完全消除因垃圾收集所引起的停顿,避免应用程序运行时发生抖动;

内存占用:垃圾收集器流畅运行所需要的内存数量;

这其中任何一个属性性能的提高几乎都是以另一个或两个属性性能的损失作代价的。换句话说,某一个属性上的性能提高总会牺牲另一个或两个属性。然而,对大多数的应用而言,极少出现这三个属性的重要程度都同等的情况。很多时候,某一个或两个属性的性能要比另一个重要。

5.2 三个基本原则

  • Minor GC回收原则:每次Minor GC都尽可能多地收集垃圾对象,遵守这一原则可以减少应用程序发生Full GC的频率。Full GC的持续时间总是最长的,是应用程序无法达到其延迟或吞吐量要求的罪魁祸首;
  • GC内存最大化原则:处理吞吐量和延迟问题时,垃圾处理器能使用的内存越大,即Java堆空间越大,垃圾收集的效果越好,应用程序运行也越流畅;
  • GC调优的3选2原则:在三个性能属性(吞吐量、延迟、内存占用)中任意选择两个进行JVM垃圾收集器调优。

调优JVM垃圾收集的过程中谨记这三条原则能帮助你更轻松地调优垃圾收集,达到应用程序的性能要求。

5.3 垃圾调优需要收集的信息

在后面的调优过程中,要根据监控垃圾收集时获得的指标决定JVM的调优方案。GC日志是收集调优所需要信息的最好途径,可以通过命令行开启HotSpot VM的GC统计信息采集功能。

为了定位问题,即使在生产系统上开启GC日志也是个不错的方案,开启GC日志对性能的影响极小。

-XX:+PrintGCTimeStamps   -XX:+PrintGCDetails -Xloggc:<filename>

-XX:+PrintGCTimeStamps打印从VM启动直到GC开始所经历的时间(单位:sec),如果需要用日历时间格式打印时间戳,可以使用-XX:+PrintGCDateStamps;

-XX:+PrintGCDetails提供垃圾收集器相关的统计数据,该选项的输出与使用的垃圾收集器密切相关,所以使用不同的垃圾收集器输出结果会有不同;

-Xloggc:<filename>选项可以指定将GC的日志信息记录到名为filename的文件中。

关于GC日志解释,请参考

针对高延迟问题调优HotSpot VM时,有两个参数可以设置:

-XX:+PrintGCApplicationStoppedTime获得应用程序由于执行VM安全点操作而阻塞的时间

-XX:+PrintGCApplicationConcurrentTime两个安全点操作之间应用程序运行的时间

日志样例如下:

应用运行了0.0776117 seconds

暂停了0.0001124 seconds

到达安全点的耗时0.0000138 seconds

注:安全点操作使JVM进入一种状态:所有的Java应用线程都被阻塞,安全点操作常用于虚拟机需要进行内部操作时,此时所有的Java线程都被显式地置于阻塞状态且不能修改Java堆的情况。

-XX:+PrintSafepointStatistics可以将垃圾收集的安全点与其他的安全点区分开来。

6. 确定内存占用

 这一步的调优将为我们定义运行应用程序需要的Java堆的大小提供有力的依据,可以知道应用程序有多少活跃数据。

活跃数据的大小是指,应用程序稳定运行时长期存活对象所占用的Java堆内存量。换句话说,它是应用程序运行于稳定态时,Full GC之后Java堆所占用的空间大小。

6.1 HotSpot VM堆的布局

具体Java堆的布局可以参考【Java内存划分】 ,可以帮助我们确定应用程序使用Java堆的大小、微调影响垃圾收集器性能的空间大小。

6.1.1 新生代

-Xms设置堆的初始和最小值,这个空间包括新生代和老年代总和。

-Xmx设置堆的最大值,这个空间包括新生代和老年代总和。

-XX:NewSize设置新生代的初始和最小值,如果设置了该参数,也应该同时设置-XX:MaxNewSize。

-XX:MaxNewSize设置新生代的最大值,如果设置了该参数,也应该同时设置-XX:NewSize。

-Xmn同时设置新生代的初始,最小和最大值,如果期望-XX:NewSize和-XX:MaxNewSize相等,则这是一个便利的参数。

备注:关注吞吐量和延迟的Java应用程序应该将-Xms和-Xmx设定为同一值。这是因为无论扩展还是缩减新生代空间或老年代空间都需要进行Full GC,而Full GC会降低程序的吞吐量并导致更长的延迟。

6.1.2 老年代

老年代空间的大小会根据新生代的大小隐式设定。

老年代的初始值为(-Xmx)- (-XX:NewSize)

老年代的最小值为(-Xmx)- (-XX:MaxNewSize)

6.1.3 Metaspace

-XX:MetaspaceSize设置元空间的初始和最小值。

-XX:MaxMetaspaceSize设置元空间的最大值。

如果不显式指定堆大小,如初始值,最大值,新生代大小,Metaspace大小,HotSpot可以通过名为“自动调优”的自适应调优功能,依据系统配置自动选择合适的值。

新生代、老年代或Metaspace这三个空间中的任何一个不能满足内存分配请求时,就会发生垃圾收集。

新生代是触发Minor GC,老年代和Metaspace触发Full GC。

Full GC会对新生代,老年代和Metaspace进行回收。

其中开启-XX:+UseParallelGC或-XX:+UseParallelOldGC时,如果关闭-XX:-ScavengeBeforeFullGC,HotSpot VM在Full GC之前不会进行Minor GC,但Full GC过程中依然会收集新生代;如果开启-XX:+ScavengeBeforeFullGC,HotSpot VM在Full GC前会先做一次Minor GC,分担一部分Full GC原本要做的工作。

6.2 堆大小调优着眼点

通过命令行-XX:+PrintCommandLineFlags可以查看堆的初始值(-XX:InitialHeapSize)和最大值(-XX:MaxHeapSize)。

如果GC日志中出现OutOfMemoryError,可以尝试通过增加JVM可用物理内存缓解。尤其要关注引起OutOfMemoryError的堆空间,确保增加其大小。例如,对老年代引起的OutOfMemoryErrors,增加-Xms和-Xmx;对Metaspace空间引起的OutOfMemoryErrors,增加-XX:Metaspace和-XX:MaxMetaspace值。

6.3 计算活跃数据大小

活跃数据大小是应用程序运行与稳定态时,长期存活的对象在Java堆中占用的空间大小。也就是说,活跃数据大小是应用程序运行于稳定态,Full GC之后Java堆中老年代和Metaspace占用的空间大小。

Java应用的活跃数据大小可以通过GC日志收集,活跃数据大小包括下面的内容:

  • 应用程序运行于稳定态时,老年代占用的Java堆大小;
  • 应用程序运行于稳定态时,metaspace占用的大小;

为了更好度量应用程序的活跃数据大小,最好在多次Full GC之后再查看Java堆的占用情况,另外,需要确保Full GC发生时,应用程序正处于稳定态。

6.4 初始堆空间大小配置

 如何根据统计的活跃数据大小,确定Java堆的初始大小。

 通用法则之一:将Java堆的初始值-Xms和最大值-Xmx设置为老年代活跃数据大小的3~4倍。

 通用法则之二:Metaspace的初始值-XX:MetaspaceSize及最大值-XX:MaxMetaspaceSize应该比Metaspace活跃数据大1.2~1.5倍。

通用法则之三:新生代空间应该为老年代空间活跃数据的1~1.5倍。

通用法则之四:如果Java堆的初始值及最大值为活跃数据大小的3~4倍,新生代为活跃数据的1~1.5倍,老年代应设置为活跃数据大小的2~3倍。

总结:计算Java堆大小可以参考下表:

空间 参数 占用倍数 备注
Java堆 -Xms和-Xmx 3~4Full GC后的老年代空间占用量  
Metaspace -XX:MetaspaceSize和-XX:MaxMetaspaceSize 1.2~1.5倍Full GC后的Metaspace占用量  
新生代 -Xmn或-XX:NewSize和-XX:MaxNewSize 1~1.5倍Full GC后的老年代占用量  
老年代 Java堆减去新生代大小 2~3倍Full GC后的老年代占用量  

6.5 其他考量因素

本节介绍的计算出来的Java堆大小并不代表Java应用程序的总内存占用。

另外,Java堆不一定是最耗应用程序内存的。例如,应用程序的线程栈可能需要较多的内存,线程的数目越多,消耗在线程栈上的内存就越多。应用程序中,方法调用的层次越深,线程栈占用的空间也越大。

谨记一点,调优过程中,这一步操作可能导致应用程序无法达到内存需求。出现这种情况,要么需要回顾或修改应用程序的内存占用需求,要么需要调整应用程序,进行Java堆的分析,修改应用程序,减少对象分配或对象保持,这些都是可以采用的处理方法。减少对象分配,或更重要的,减少对象保持可以减少活跃数据的大小。

这一步中计算出的Java堆大小仅仅是一个出发点。

7. 调优延迟/响应性

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

这一步调优有两个可能的结果:

  • 应用程序的延迟性达到要求
  • 应用程序的延迟性达不到要求:需要回顾应用程序的延迟性要求,或修改应用程序以改善延迟性,可以采用如下的方式:(1)堆分析,修改应用程序,减少对象分配及对象保持(2)改变JVM的部署模式,减少单个JVM的负荷;任何一个都可以减少JVM的对象分配率,并随之减少GC的频率

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

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

测量Minor GC的持续时间和频率堆优化Java堆的大小至关重要,Minor GC的持续时间和频率决定了优化后新生代的大小;

最差情况下的Full GC持续时间和频率决定了老年代的大小和垃圾收集器的切换:是否需要从PS收集器转向CMS收集器。

 应用程序的系统性需求:

  • 应用程序可接受的平均停顿时间:平均停顿时间将与测量出的Minor GC持续时间进行比较;
  • 应用程序可接受的应用程序的最大停顿时间:最大停顿时间与最差情况下Full GC的持续时间进行比较;
  • 应用程序可接受的最大停顿发生的频率:最大停顿时间的频率基本上就是Full GC的频率,对应大多数应用程序,相对于GC的频率,更关心GC持续的平均停顿时间和最大停顿时间。

7.1 优化新生代的大小

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

Minor GC需要的时间与新生代中可访问的对象数直接相关,通常情况下,新生代空间越小,Minor GC持续的时间越短,减少新生代空间又会增大Minor GC的频率。以同样的对象分配速度,较小的新生代空间在很短的时间内就会被填满,增大新生代空间可以减少Minor GC的频率。

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

真正达到应用程序的平均延迟要求之前可能要经历多次迭代,调整新生代空间大小时,尽量保持老年代空间大小恒定。

如何估算增加多大的新生代来满足GC频率的要求?

实例:假设应用程序的Minor GC频率要求是每5秒一次,目前Minor GC的评论是每2.147秒一次,由于应用程序的Minor GC频率要求低于计算出的频率,我们可以增加新生代的空间大小。根据新生代空间的当前大小和平均Minor GC的频率,能够大致估算出可以增加多少新生代空间。

(1)这个例子中,填充满2048MB新生代空间平均耗时2.147;

(2)假设对象分配速度是恒定的,那么需要增加2.3 = 5/2.147秒;

(3)也就是说如果2.147秒可以填满2048MB空间,那么5秒可以填满大约4700MB空间;

(4)因此,为了达到5秒的Minor GC频率目标,新生代空间大小需要调整为4700MB。

 调整新生代空间时,要注意下面的几个准则:

  • 老年代空间大小不应该小于活跃数据大小的1.5倍;
  • 新生代空间至少为Java堆大小的10%,通过-Xmx和-Xms可以设置改值(新生代过小会导致频繁GC);
  • 增大Java堆大小时,需要注意不要超过JVM可用的物理内存数(堆占用过多内存将导致底层系统交换到虚拟内存,反而会造成垃圾收集器和应用程序的性能低下)。

如果只考虑Minor GC引起的延迟,而调整新生代的大小又无法满足应用程序的平均停顿时间或延迟性要求,就只能修改应用程序或改变JVM的部署模式,在多个JVM上部署应用程序,或吸怪应用程序的平均延迟性要求。

7.2 优化老年代的大小

评估Full GC引入的最差停顿时间和Full GC的频率。

发生于稳定态的Full GC的持续时间是应用程序的最差Full GC停顿时间,对Full GC频率的预估应该依据对象提升率进行计算。提升率可以依据老年代空间占用的增长量和每次Minor GC后新生代的空间占用计算得出,老年代空间占用情况可以通过Minor GC之后Java堆的占用情况减去同一次Minor GC后新生代的空间占用得到。

 实例:有如下的Minor GC日志

从日志可以看出,Java堆的带席位6144MB,新生代大小为2048MB,老年代为6144 - 2048 = 4096MB;

假设活跃数据大小为1370MB,则老年代中有空闲空间4096 - 1370 = 2726MB,需要多长时间才能填满2726MB?

每次Minor GC之后老年代的占用情况:

每次Minor GC之后老年代的增长量:

除了提升率,还需要知道Minor GC的频率,假设平均Minor GC的频率是每隔2.147秒一次。因此提升率为21494KB / 2.147,大约为10MB/sec。因此填满2726MB可用老年代的时间大约为272.6秒。

因此,根据前面的GC分析,应用程序可以预期的最差Full GC频率是每272.6秒一次。

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

 如果老年代过小,有可能会出现频繁Full GC,老年代中几乎没有任何空间被回收,与此同时,新生代中总有大量的对象占用空间,当老年代中空间无法接纳从新生代中提升的对象时,这些对象会被“退还”到新生代空间中。

8. 应用程序吞吐量调优

 应用程序的吞吐量通常在应用层面而不是在JVM的层面进行度量。

8.1 调优CMS吞吐量

见9.6小结

8.2 调优PS吞吐量

对PS收集器进行吞吐量性能调优的目标是尽可能避免发生Full GC或更理想的情况下在稳定态时永远不发生Full GC。为了达到这个目标需要优化对象老化频率,通过显式地微调Survivor空间可以实现对象老化的优化。你可以将Eden空间变得更大,从而降低Minor GC的频率,确保老年代有足够的空间持有应用程序的活跃数据。如果对象没有理想的老化频率,一些非长期存活对象被提升到了老年代时,可以增加一些额外的老年代空间来应对这种情况。

PS收集器提供的吞吐量性能是HotSpot VM诸多垃圾收集器种最好的。PS收集器默认启用了一个称为自适应大小调整的特性。自适应大小调整根据对象分配以及存活率自动地对新生代Eden和Survivor空间进行调整以最优化对象老化频率。使用如下的选项可以禁用自使用大小调整:-XX:-UseAdaptiveSizePolicy。只有PS收集器支持自适应大小调整。尝试在非PS收集器上启用或禁用自适应大小调整都不会有任何效果,即这种操作时空操作。

-XX:+PrintAdaptiveSizePolicy(无论UseAdaptiveSizePolicy是否开启)可以生成更详细的Survivor空间占用日志。实例如下:

survived:标示To Survivor空间中存活对象的大小,即minor GC之后,To Survivor空间的空间占用的空间大小;

promote: 新生代提升至老年代空间的对象大小;

overflow:是否有Survivor空间的对象溢出到老年代空间,也就是说To Survivor空间是否有足够的空闲空间容纳垃圾收集时Eden和From Survivor空间中的幸存对象。

为了达到最优吞吐量性能,理想情况下,应用程序运行于稳定态时,Survivor空间不应该发生溢出。

如果Survivor空间在稳定态发生溢出,对象将在其达到极限年龄老化死去之前被提升到老年代空间。频繁的Survivor空间溢出会导致频繁的Full GC。

 

9. CMS垃圾收集器调优

使用CMS收集器,老年代垃圾收集线程和应用程序线程能实现最大的并行度,这为同时降低最差延迟出现的频率以及最差延迟的持续时间,避免发生长时间的GC提供了机会。

CMS并不进行压缩,所以这一效果主要是通过避免老年代空间发生STW压缩式垃圾来收集实现的。一旦老年代溢出就会触发STW压缩式垃圾收集。

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

调优CMS收集器的目的是避免发生STW的压缩式GC。

使用CMS,如果老年代空间用尽,就会触发一个单线程STW压缩式的垃圾收集。

相对于Parallel Old收集器的Full GC而言,CMS垃圾收集通常的持续时间更长。

几个方面因素使得CMS收集器的调优非常具有挑战性:

(1)对象从新生代提升至老年代的苏联;

(2)并行老年代垃圾收集线程回收空间的速率;

(3)由于CMS收集器回收位于对象之间的垃圾对象而造成老年代空间的碎片化

解决碎片化的方法有:(1)压缩老年代空间(2)加大老年代空间(3)减少对象从新生代提升到老年代的比率

9.1 Survivor空间

9.1.1 介绍

Survivor空间时新生代空间的一部分。具体参考http://www.cnblogs.com/lujiango/p/7690885.html

备注:跟CMS不同,Parallel Old收集器默认开启一个“自适应大小调整”的功能,能够自动调整Eden和Survivor大小。但是通用的操作时相同的,比如对象如何分配,如何从Eden复制到Survivor空间,如果在Survivor之间复制跟CMS收集器是一致的。

在所有的HotSpot垃圾收集器种,新生代空间都被划分成一个Eden和2个Survivor。一块标记为From Survivor空,另一块标记为To Survivor空间。

Eden空间时分配新Java对象的空间,例如一个Java程序中有下面的语句:

Map<String, Long> map = new HashMap<String, Long>();

这行语句会在Eden空间分配一个新的HashMap对象,HashMap构造器中的对象也会保存在Eden空间中,当Eden空间被填满时就会发生Minor GC。活跃对象会从Eden空间复制到标记为To的Survivor空间,同时FromSurvivor空间中存活下来的对象也会复制到To Survivor空间中。一旦完成Minor GC, Eden空间会清空,From Survivor空间也变为空,而To Survivor空间中保存了还活跃的对象。之后,Survivor空间将互相交换标记为下一次的Minor GC做准备。现在已清空的From Survivor空间换上了To标示,而To Survivor空间换成From标示。因此,Minor GC结束时,Eden空间和一块Survivor空间变为空,另一块Survivor空间中保存着经历了上次Minor GC存活下来的活跃对象。

如果Minor GC时,To Survivor空间不足以容纳所有从Eden空间和From Survivor空间中复制过来的活跃对象,超出的部分会提升至老年代空间。

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

-XX:SurvivorRatio=<ratio>标示单个Survivor空间同Eden空间的大小比率。

Survivor空间的计算公式为:

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

9.1.2 调节Survivor空间的容量

调整Survivor空间容量一个英国谨记于心的重要原则:调整Survivor空间容量时,如果新生代空间大小不变,增大Survivor空间会减少Eden空间;而减少Eden空间会增大Minor GC的频率。

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

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

9.1.3 调优目标Survivor空间占用

目标Survivor空间占用时VM尝试在Minor GC之后仍然维持的Survivor空间占用。通过VM命令行选项-XX:TargetSurvivorRatio=<percent>可以对该值进行调整。通过命令行选项指定的参数实际上是Survivor空间占用的百分比而不是一个比率,默认值为50%。

HotSpot VM团队对不同类型的应用程序进行了大量的测试,结果表明50%的目标Survivor空间占用能适应大多数的应用程序。这是因为它能应对Minor GC时存活对象的急剧增加。极少需要对目标Survivor空间占用进行调优的情况,但是如果应用程序有一个相对稳定的分配速率可以考虑提高目标Survivor空间占用到80~90。这样可以减少用于老化对象的Survivor空间的数量。

 

9.2 晋升阈值

9.2.1 解析晋升阈值

为了对Survivor空间做更细致的调整,优化新生代堆的大小,需要监控晋升阈值。晋升阈值决定了对象在新生代Survivor空间中保留的时间。

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

注:新生代中的有效对象老化可以避免将不成熟的对象提升到老年代空间,减少了老年代空间的占用率增长。

可以使用HotSpot VM的命令行选项-XX:MaxTenuringThreshold=<n>指定最大晋升阈值。

如果值设置的太小:可能造成分配的对象在几次Minor GC之后从新生代提升到老年代,造成老年代空间的迅速增加,引起频繁的Full GC;

如果值设置的太大:可能造成对象长期存在于Survivor空间,直到最后溢出,一旦发生溢出,对象将被全部提升至老年代,不再依据其实际年龄进行提升,这样会造成短期存在对象在长期存在对象之前被提升到老年代,严重影响对象老化机制的有效性。

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

9.2.2 监控晋升阈值

使用命令行选项-XX:+PrintTenuringDistribution可以监控晋升的分布或对象年龄分布。并以此为依据确定最优的最大晋升阈值。

在输出中,需要关注的是随着对象年龄的增加,各对象年龄上字节数减少的情况,以及VM计算出的晋升阈值是否等于或接近设置的最大晋升阈值。

实例:

 

最大晋升阈值为15,VM内部计算出的晋升阈值为1。Desired survivor size是Survivor空间的大小乘以目标存活率得到的空间大小。目标存活率是VM预计目标空间在Survivor空间中占用的百分比。每个年龄的对象及其占用的空间大小单独列为一行,本例中,年龄为1的对象大小为1669048节。同时在每一行中会列出对象总的大小(字节数)。如果出现多年龄行的情况,总大小是该年龄行及其之前所有对象大小的累积之和。

 本例中,期望Survivor空间大小为8388608远小于存活对象大小16690480,导致Survivor空间溢出,即最终Minor GC将一些对象提升至老年代。Survivor空间溢出表明Survivor空间过小。

 9.3 调优CMS手机周期

减少最差情况的延迟并最小化最差延迟发生的频率,这一步的目标是维持空闲老年代空间的恒定,并由此避免发生STW压缩式垃圾收集。

STW压缩式垃圾手机是引入延迟的最大的垃圾手机。在一些应用中,这可能是无法完全避免的,但是我们可以降低它们发生的频率。

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

碰到STW压缩式垃圾收集,可以尝试调节CMS周期启动的时间,CMS中发生的STW压缩式垃圾收集的垃圾收集日志中可以通过查找并发模式失效定位(Concurrent Mode Failure)。

 可以通过下面的命令行通知VM在更早的时间启动CMS垃圾收集周期:-XX:CMSInitiatingOccupancyFraction=<percent>

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

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

9.4 CMS显式的垃圾收集

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

(1)可以使用如下的VM命令行选项,-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses。

(2)页可以使用下面的命令行通知VM忽略显式的System.gc()调用:-XX:+DisbaleExplicitGC

注意:使用这个命令行选项也会导致其他VM的垃圾收集器忽略显式的System.gc()调用。

9.5 调优CMS停顿时间

CMS周期中有2个阶段是STW的阶段,处于这2个阶段(初始标记和重新标记)的应用程序线程会被阻塞。虽然初始标记阶段是单线程的,却极少占很长时间,通常情况下远小于其他的垃圾收集停顿。重新标记阶段是多线程的。可以通过VM命令行选项控制重新标记阶段使用的线程数-XX:ParallelGCTheads=<n>,如果Runtime.availableProcessors()的返回值<=8,-XX:ParallelGCThreads默认等于这个值;否则,该默认值为8 + (Runtime.availableProcessors() - 8)* 5 / 8。

重新标记阶段的持续时间在某些时候可以通过下面的选项设置:-XX:+CMSScavengeBeforeRemark该选项强制VM在进入CMS重新标记阶段之前先进行一次Minor GC。重新标记之前的MInor GC通过减少老年代空间的新生代对象数目,将重新标记阶段的工作量减到了最少。

9.6 调优CMS吞吐量

使用CMS收集器时,为了获得更大的吞吐量性提升你需要使用一系诶配置选项。

  • 增加新生代空间大小。降低Minor GC频率。
  • 增加老年代空间的大小。降低CMS周期的频率并减少内存碎片。
  • 进一步优化新生代堆的大小,调整新生代中Eden空间和Survivor空间的大小以优化对象老化,减少由新生代提升到老年代的对象数目,最终减少CMS周期的发生数。
  • 进一步优化CMS周期的启动条件。

以上任何一个选项或几个选项的组合都可以减少垃圾收集器消耗的CPU周期数,从而将更多的CPU周期用于执行应用程序

指导原则:CMS包括Minor GC所带来的开销应该小于10%,你可能将这个值减少到1%~3%,通常情况下,如果当前观察到CMS垃圾收集的开销在3%或更少,通过调优吞吐量性能提升的空间就极其有限了。

 

 

 

 

参考资料

Java性能优化权威指南

 

posted @ 2017-12-23 17:25  小路不懂2  阅读(429)  评论(0编辑  收藏  举报