1.应用程序的系统需求:
应用程序的系统需求是应用程序运行时某方面的要求,譬如吞吐量、响应时间、内存消耗量、可用性、可管理性等。JVM性能调优主要针对如下的系统需求:
(1).可用性:
是对应用程序处于可操作、可使用状态的度量。可用性需求指的是当程序的某些组件发生故障或失效时,应用程序或应用程序的一部分在多大程度上海可以继续提供服务。
Java应用程序的上下文中,利用应用程序组件化、在多个JVM中运行或在多个JVM上运行多个应用程序实例都可以实现高可用性。强调高可用性的代价之一是管理成本的增加,引入更多的JVM意味着要管理更多的JVM,从而导致复杂性增加以及随之而来的管理成本。
(2).可管理性:
可管理性是对由运行、监控应用程序而产生的操作性开销的度量,同时也包含了配置应用程序的难易程度。可管理性需求用于衡量系统管理的难易程度。
(3).吞吐量:
吞吐量是堆单位时间内处理工作量的度量。设计吞吐量需求时,一般不考虑它对延迟或者响应时间的影响。
通常情况下,增加吞吐量的代价是延迟的增加或者内存使用的增加。
(4).延迟及响应性:
延迟或者响应性是指应用程序收到指令开始工作直到完成该工作所消耗时间的度量。
定义延迟及响应性需求时,一般不考虑程序吞吐量,通常情况下,提高响应性或缩小延迟的代价是更低吞吐量、或者更多的内存消耗(或者二者同时发生)。
(5).内存占用:
内存占用是指在同等程度的吞吐量、延迟、可用性和可管理性前提下,运行应用程序所需的内存大小。
内存占用通常以运行应用程序所需的java堆大小或者运行应用程序所需的总内存大小来表述。一般情况下,通过增大java堆的方式增加可用内存能够提供吞吐量、降低延迟或者兼顾二者。
(6).启动时间:
启动时间是应用程序初始化所消耗时间。此外,java应用程序中另一个值得关注的指标是现代JVM完成应用程序热区优化,初始化所消耗的时间。
Java应用程序初始化的完成时间取决于很多因素,包括(但不限于):初始化时载入的类的数量、需要初始化的对象的数量、这些对象如何初始化以及HotSpot VM的运行时环境选择,即Client模式还是Server模式。
2.JVM部署模式选择:
JVM部署模式的选择是指将应用程序部署到单个JVM实例上,还是部署到多个JVM实例上。
(1).单JVM部署模式:
将java应用部署在单个JVM上时,由于不需要管理多个JVM,可以降低管理成本,此外每个部署的JVM都有相应的内存开销,使用单JVM避免了这部分资源使用,应用程序所消耗的总内存数量会减少。
单JVM部署模式存在单点故障,即当应用程序遭遇灾难性错误或JVM失效时,无法保证应用程序的可用性。
另外,如果应用内存消耗超过了32位JVM处理能力时,需要使用64位JVM,需要确认所使用的第三方模块是否支持64位JVM,如果还使用了JNI,需要使用64位编译器编译。
(2).多JVM部署模式:
将java应用部署到多个JVM实例能够获得更好的可用性,以及更低延迟的可能性。采用多JVM部署模式时,单一应用程序或JVM实例的失效只会影响整个应用程序的部分功能。同时由于在多JVM部署模式下,java堆通常比较小,较小的堆在垃圾收集时产生的停顿更小,因此多JVM部署模式可能提供更低的延迟。另外,采用多JVM部署模式由于将负荷分发到多个JVM,能够改善应用程序的扩展性,从而处理更高负荷,提供吞吐量。
3.JVM运行模式选择:
(1).Client/Server模式:
A.Client模式:特点是启动快、占用内存少、JIT编译器生成代码的速度也更快,但是JIT编译优化的代码质量不如Server模式高。
B.Server模式:JIT编译优化需要消耗额外的时间以收集更多的应用程序行为,启动时间更长,但是能生成高度优化的机器码。
C.Tiered Server模式:在JDK7中正式发布,结合了Client和Server模式的优点,即快速启动和高效的生成码。如果使用的是java7或更新的版本的JVM,可以使用”-server -XX:+TieredCompilation”命令行选项启动Tiered Server模式。
(2).32位/64位:
HotSpot VM默认使用32位运行模式,但是服务器的硬件配置、第三方库是否支持64位JVM以及JNI是否使用64位编译也决定是否适合使用64位JVM。
A.32位JVM:
默认的HotSpot VM运行模式,java堆内存小于2G的推荐选择。
B.64位JVM:
当java堆介于2G~32G时,使用”-d64 -XX:+UseCompressedOops”命令行选项的64位JVM。
当java堆大于32G时,使用”-d64”命令行选项的64位JVM。
注意:使用” -XX:+UseCompressedOops”命令行选项的HotSpot VM,最大java堆小于等于26G时性能最好,java6 Update18之后的HotSpot VM能根据最大java堆的情况自动启用它。
(3).垃圾收集器选择:
JVM的垃圾收集器选择也影响着系统需求,HotSpot VM提供了多种垃圾收集器,很多情况下使用Throughput或CMS垃圾收集器就可以满足系统对吞吐量、响应性等需求的要求。
4.垃圾收集器调优基础:
(1).垃圾收集器调优的性能属性:
A.吞吐量:
评价垃圾收集器的重要指标之一,指不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标。
B.延迟:
也是评价垃圾收集器能力的重要指标,度量标准是缩短由于垃圾收集引起的停顿时间或完全消除因垃圾收集所引起的停顿,避免应用程序运行时发生抖动。
C.内存占用:
垃圾收集器流畅运行所需要的内存数量。
上述3个性能属性的任何一个提高,都会对其他的一个或两个性能属性带来损失,因此需要根据应用场景确定上述性能属性的优先级。
(2).垃圾收集器调优原则:
A.Minor GC回收原则:
每次Minor GC都尽可能多地收集垃圾对象,遵循这一原则可以减少Full GC的频率,Full GC的持续时间总是最长的,是应用程序无法达到其延迟或吞吐量要求的罪魁祸首。
B.GC内存最大化原则:
处理吞吐量和延迟问题时,垃圾处理器能使用的内存越大,即java堆空间越大,垃圾收集的效果越好,应用程序运行也约流畅。
C.GC调优的3选2原则:
在三个性能属性(吞吐量、延迟和内存占用)中任意选择两个进行JVM垃圾收集器调优。
5.确定内存占用:
(1).计算活跃数据大小:
活跃数据的大小是指,应用程序稳定运行时,长期存活对象所占用的java堆内存量,即它是应用程序运行于稳定态时,Full GC之后java堆占用的空间大小(包括老年代活跃数据和永久代活跃数据)。活跃数据大小是确定运行应用程序所需java堆大小的切入点。
为了更好度量应用程序的活跃数据大小,最好在多次Full GC之后在查看java堆占用情况,另外,需要确保Full GC发生时,应用程序正处于稳定态。
对于发生Full GC或者不经常发生Full GC的应用程序来说,可以使用JConsole、VisualVM或者jmap(需要使用”jmap -histo:live PID”命令)人工触发Full GC。
(2).初始堆空间大小配置:
通过计算活跃数据大小之后,下面是初始堆空间大小配置的通用原则:
A.java堆:
使用-Xmx和-Xms将java堆初始大小设置为老年代活跃数据大小的3~4倍。
B.永久代:
使用-XX:PermSize和-XX:MaxPermSize将永久代初始大小设置为永久代活跃数据大小的1.2~1.5倍。
C.新生代:
使用-Xmn将新生代初始大小设置为老年代活跃数据大小的1~1.5倍。
D.老年代:
老年代空间大小=java堆-新生代大小,老年代应设置为老年代活跃数据大小的2~3倍。
6.调优延迟/响应性:
(1).确定延迟/响应性系统要求:
A.应用程序可接受的平均停滞时间。
B.应用程序可接受的Minor GC(会导致延迟)频率。
C.应用程序可接受的最大停顿时间。
D.应用程序可接受的最大停顿发生频率。
(2).评估垃圾收集对延迟/响应性影响因素:
A.测量Minor GC的持续时间。
B.统计Minor GC的频率。
C.测量Full GC的最差(最长)持续时间。
D.统计最差情况下,Full GC的频率。
(3).优化新生代大小:
Minor GC需要的时间及频率与新生代密切相关,通常情况下,新生代空间越小,Minor GC的持续时间越短,但是Minor GC的频率会越大;新生代空间越大,Minor GC的发生频率越低,但是Minor GC的持续时间越长。
调整新生代空间的原则:
A.老年代空间:
老年代空间大小不应该小于老年代活跃数据大小的1.5倍,在因调整Minor GC持续时间和频率而调整新生代大小时,要确保老年代大小保持不变,即同步调整java堆和新生代大小。
B.新生代空间:
新生代空间至少应为java堆大小的10%。
C.java堆空间:
java堆空间大小不要超过JVM可用物理内存。
(4).优化老年代大小:
发生于稳定态的Full GC持续时间是应用程序的最差Full GC持续时间,Full GC的时间间隔是最差Full GC频率。
调整老年代大小原则:
A.调整老年代大小时,确保新生代大小保持不变,即同步调整java堆大小和老年代大小。
B.Full GC频率的预估应该依据对象的提升率进行计算:
对象提升率是对象从新生代复制到老年代对象的比率,计算方法如下:
首先,观察Full GC之前的连续多个Minor GC中老年代空间占用。
其次,计算相邻Minor GC老年代空间占用。
第三,计算Minor GC对象平均提升速度=相邻Minor GC老年代空间占用/Minor GC间隔时间。
第四,估算Full GC频率=老年代剩余空间/Minor GC对象平均提升速度。
7.CMS调优延迟/响应性:
CMS垃圾收集器中Stop-The-World的压缩式GC与Full GC有细微区别:若老年代没有足够空间处理来自新生代的对象晋升即老年代溢出,则只会在老年代触发一次Stop-The-World的压缩式GC;发生Full GC时,触发使用了-XX:-ScavengeBeforeFullGC命令行选项,否则老年代和新生代都会进行垃圾收集。
由于Stop-The-World的压缩式GC对老年代空间进行压缩,耗时比较长,因此调优CMS收集器的目的是避免Stop-The-World的压缩式GC。
(1).调用新生代的Survivor空间:
HotSpot VM的新生代空间通常分为三部分:Eden、Survivor From和Survivor To,其中Survivor From和Survivor To是相互互换的,新分配的对象通常存放在Eden和Survivor From中,Minor GC之后存活的对象会存放在Survivor To中,合适大小的Survivor空间可以使得Minor GC回收最多垃圾对象,避免短期对象晋升到老年代,增加老年代碎片。
A.Survivor空间的大小:
可以通过-XX:SurvivorRation=
Survivor空间大小=-Xmn
B.Survivor空间调整:
对于给定新生代,减少Survivor比率会增大Survivor空间,同时减少Eden空间,导致更频繁的Minor GC,即垃圾收集的频率越高,对象老化的速度越快;增大Survivor比率会减少Survivor空间,同时增大Eden空间,减少Minor GC频率,即垃圾收集的频率越低,对象老化的速度越慢。
C.调整Survivor空间原则:
若可以增加Minor GC频率,可以减少一部分Eden空间来增大Survivor空间;若内存足够,最好增加新生代大小,保持Eden空间恒定的同时增加Survivor空间。
调整Survivor空间要和监控对象晋升年龄阀值一起配合。
(2).调整对象晋升年龄阀值:
一个对象的年龄是它所经历的Minor GC次数,新生代空间中年龄大于晋升阀值的对象都会被从新生代提升到老年代空间,即晋升年龄阀值决定了对象在新生代Survivor空间保留的时间。
新生代中的有效对象老化可以避免将不成熟的对象提升到老年代空间,减少了老年代空间的占用率增长,同时也降低了CMS垃圾收集器的执行频率,减少了可能的老年代空间碎片。
A.监控对象晋升年龄阀值:
使用-XX:+PrintTenuringDistribution选项可以监控对象晋升的发布或对象年龄发布,输出结果如下:
Desired survivor size 3506176 bytes, new threshold 1 (max 15)
- age 1: 7012344 bytes, 7012344 total
从上面输出可以看出来默认的晋升阀值为15,由(max 15)标识;通过new threshold 1 可以知道虚拟机内部计算出的晋升阀值为1;Desired survivor size 3506176 bytes是Survivor空间大小乘以目标存活率(由-XX:TargetSurvivorRation=
由上面例子看出期望的Survivor空间3506176 bytes远小于存活对象大小7012344 bytes,导致Survivor空间溢出,从而使得Minor GC将一些对象提升到老年代。
观察到Survivor空间过小时,调整Survivor空间大小,使得总存活对象小于等于期望的Survivor空间大小,同时使得晋升阀值等于最大晋升阀值。
B.调整对象晋升年龄阀值:
使用-XX:MaxTenuringThreshold=
设置原则:
不建议将其设置为0,这会造成刚刚分配的对象在随后的Minor GC中直接从新生代提升到老年代,引起老年代空间迅速增长,导致频繁的Full GC;
也不建议将其设置为远大于实际可能的最大值,这会造成对象长期存在于Survivor空间,直到最后Survivor空间溢出,一旦Survivor空间溢出,对象将被全部提升至老年代,不再依据其实际年龄进行提升,导致短期对象在长期对象之前被提升至老年代,严重影响对象老化机制的有效性。
(3).调用老年代空间占用率:
CMS的新生代空间和晋升年龄阀值调整好之后,就可以保证大部分垃圾对象在Minor GC时被回收,同时保证对象有效老化,即老年代空间占用率增长可控,接下来就是要调整老年代空间占用率,以确保避免老年代发生Stop-The-World的压缩式GC。
成功的CMS收集器性能调优要能以对象从新生代提升到老年代的同等速度对老年代对象进行垃圾收集,达不到这个标准则称之为”失速”(Lost the race),失速的结果就会发生Stop-The-World的压缩式GC。避免失速的关键是要结合足够大的老年代空间和足够快地初始化CMS垃圾收集周期,让它比提升速率更快的速度回收空间。
CMS周期的初始化周期基于老年代空间占用情况,如果CMS开始的太晚,就会发生失速;但是如果CMS周期启动的太早,又会引起无用的消耗,影响应用程序的吞吐量。
HotSpot VM会尝试自适应第计算在空间占用多大时开启CMS收集周期,但是做的并不理想,某些情况下无法避免会出现Stop-The-World的压缩式GC,通过在GC日志中查找并发模式失效(Concurrent Mode Failure)定位Stop-The-World的压缩式GC,如果发生Stop-The-World的压缩式GC,可以使用如下方式配置老年代空间占用率以启动CMS周期:
A.-XX:CMSInitiatingOccupancyFraction=
设定的值是CMS垃圾收集周期在老年代空间占用达到多少百分比时启动,仅在第一个CMS周期中使用设定的比率,之后周期又转为自适应启动CMS周期。
设置的原则为至少应该是老年代活跃数据大小的1.5倍。
B.-XX:+UseCMSInitiatingOccupancyOnly:
与-XX:CMSInitiatingOccupancyFraction=
(4).调优CMS对永久代收集:
Full GC也可能源于永久代空间用尽,通过监控GC日志,可以判断Full GC是否由永久代耗尽引起。
在CMS中,HotSpot VM默认情况下不会对永久代空间进行垃圾收集,通过以下的命令行选项,可以控制CMS对永久代的收集:
A.开启CMS对永久代的垃圾收集:
-XX:+CMSClassUnloadingEnabled,Java6 Update3或更新的版本,还可以使用-XX:+CMSPermGenSweepingEnabled与-XX:+CMSClassUnloadingEnabled一起配合使用。
B.调整永久代占用空间:
和老年代类似,开启CMS对永久代垃圾收集之后,也可以通过设定永久代空间占用,来控制CMS在永久代的垃圾收集,参数如下:
-XX:CMSInitiatingPermOccupancyFraction=
指定永久代空间占用率达到多少百分比时启动CMS在永久代垃圾收集,仅在仅在第一个CMS周期中使用设定的比率,之后周期又转为自适应启动CMS周期。
-XX:+UseCMSInitiatingOccupancyOnly:
与-XX:CMSInitiatingPermOccupancyFraction=
(5).调优CMS停顿时间:
CMS周期中有两个阶段是Stop-The-World的阶段:初始标记和重新标记,处于这两个阶段的应用程序会被阻塞,虽然初始标记是单线程的,却极少占很长的时间,通常情况下远小于其他的垃圾收集停顿,重新标记是多线程的,可以通过下面的HotSpot VM命令行选项控制:
A.-XX:ParallelGCThreads=
控制重新标记阶段使用的线程数,若Runtime.getRuntime().availableProcessors()的返回值小于等于8,则默认等于这个值;否则,该默认值=8+(Runtime.getRuntime().availableProcessors() - 8) * 5 / 8。
多个应用程序运行于同一系统的情况下,建议将其设置的小于默认值,否则由于大量的垃圾收集线程同时执行,应用程序性能会受到极大的影响。
B.-XX:+CMSScavengeBeforeRemark:
强制HotSpot VM在进入CMS重新标记阶段之前先进行一次Minor GC,减少引用老年代空间的新生代对象数目,减少重新标记阶段的工作量。
C.-XX:+ParallelRefProcEnabled:
如果应用程序有大量的引用对象或可终结对象要处理,使用该命令行选项可以减少垃圾收集的持续时间。
8.吞吐量调优:
(1).CMS吞吐量调优:
CMS吞吐量调优与其延迟调优类似,简单原则如下:
A.增加新生代大小,降低Minor GC频率。
B.增加老年代大小,降低CMS周期频率并减少内存碎片,从而减少并发模式失效以及Stop-The-World的压缩式GC发生几率。
C.调整新生代中Eden和Survivor空间大小以优化对象老化,减少由新生代提升到老年的对象数目,最终减少CMS周期发生数。
D.优化CMS周期启动条件,通过调整老年代和永久代空间占用,尽可能晚启动CMS周期。
CMS吞吐量调优的指导原则是CMS包括Minor GC所带来的开销应该小于10%,尽可能将开销降低到1%~3%。
(2).Throughput吞吐量调优:
Throughput收集器是以吞吐量最优为目标的垃圾收集器,对于其吞吐量调优的目标是尽可能避免发生Full GC,或者更理想的情况下是在稳定态时永远不要发生Full GC。
Throughput收集器默认启用了一个称为自适应大小调整的特性,根据对象分配以及存活率自动地对新生代的Eden和Survivor空间进行调整以最优化对象老化频率。自适应调优既易于JVM调优,又达到足够的吞吐量,可以满足绝大多数应用程序的要求,对于一些需要竭尽全力提高吞吐量的应用,可以通过如下方式禁用自适应而手动调整Eden和Survivor空间大小:
A.-XX:-UseAdaptiveSizePolicy:
该命令选项用于禁用自适应大小调整特性,只对Throughput收集器有效。
禁用它将会牺牲改变应用程序行为的灵活性。
B.-XX:+PrintAdaptiveSizePolicy:
该命令选项用于打印Survivor空间占用日志,通常与XX:+PrintGCDetails 、-XX:+PrintGCDateStamps或-XX:+PrintGCTimeStamps等命令行选项一起配合使用,监控GC日志中Survivor空间占用情况,为手动调整Eden和Survivor空间大小提供依据,例子如下:
2015-05-20T11:01:59.507+0800: [GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:
survived: 651584
promoted: 1622112
overflow: true
[PSYoungGen: 4160K->636K(4800K)]
4160K->2220K(15744K), 0.0057260 secs]
[Times: user=0.00 sys=0.00, real=0.01 secs]
GCAdaptiveSizePolicy标签表明GC日志是以-XX:+PrintAdaptiveSizePolicy选项开启的。
Survived标签后面的651584是Survived To空间中存活对象大小。
Promoted标签后面的1622112是由新生代提升至老年代空间的对象大小。
Overflow标签表明是否Survived空间溢出,即有新生代对象提升至老年代空间。
通过对Throughput收集器的Survivor空间不断调整和监控,设置合理的Survivor空间大小,使得稳定运行的应用程序尽可能少地发生Survivor空间溢出。
Throughput收集器调优原则是垃圾收集器的开销应该小于5%,如果可以将垃圾收集器的开销减少到1%甚至更少,就达到极限了。
(3).NUMA系统上调优:
NUMA系统是非一致性内存架构,支持NUMA的JVM会根据NUMA节点划分堆,线程创建对象时,只会在该线程所在核的NUMA节点上分配对象,后续该线程如果需要使用这个对象,就直接从本地内存中访问,因此可能会发生跨CPU和跨内存节点访问。通常情况下,如果没有使用命令(RHEL下使用numactl)设置CPU亲和性(Affinity),默认就跨多个内存节点。
如果跨CPU或内存节点访问,HotSpot为Throughput收集器提供-XX:+UseNUMA选项,根据CPU与内存位置的关系在分配线程运行的本地内存中分配对象。
9.其他优化策略:
(1).实验性(最近最大)优化:
新的性能优化集成到HotSpot VM时,常常首先在命令行选项-XX:+AggressiveOpts中引入,如果新的优化选项经过证明足够稳定,就会被加入到默认配置中,如果应用程序的性能要求高于稳定性要求,可以尝试启用实验性优化。
(2).逃逸分析:
逃逸分析(Escape Analysis)是一种评估java对象可见范围的技术,若由某个线程创建的java对象在另一个线程中可以访问,就被称之为该对象逃逸了,若java对象不发生逃逸,就可以使用其他方法进行调优,这种优化技术被称为逃逸分析。
HotSpot VM使用-XX:+DoEscapeAnalysis命令选项开启逃逸分析(在Java6 Update23之后默认开启),借助逃逸分析,JIT编译器可以做如下优化:
A.对象展开:在可能直接回收的空间而非java堆上分配对象字段,例如对象字段可以直接存放在CPU寄存器中,或者直接在栈上而不是java堆上分配对象。
B.标量替换:减少内存访问的优化技术,把java对象拆散为基本类型的字段直接在CPU寄存器中分配,减少内存访问次数。
C.栈上分配:在线程的栈帧上而非java堆中分配对象,减少垃圾收集频率。
D.消除同步:若线程分配的对象不会发生逃逸,且该线程持有该对象上的锁,由于其他线程不会访问该对象,因此可以通过JIT编译器移除锁。
E.消除垃圾收集的读/写屏障:只有线程分配的对象在另一线程中被访问时才需要读/写屏障,若该对象只能由线程本地的根节点访问,则在其他对象中存储其地址时不需要执行读/写屏障。
(3).偏向锁:
偏向锁是偏向于最后获得对象锁线程的优化技术,即当一个线程刚刚获得对象锁,当下一次再获取该对象锁时HotSpot认为该线程还能获取成功而直接把对象锁交予该线程的一种优化技术,当只有一个线程锁定该对象,没有锁冲突的情况下,其锁开销可以解决无锁。
偏向锁对于大多数应用性能提高很多,可以使用-XX:+UseBiasedLocking开启偏向锁(在JDK中默认开启);但对于有锁切换的应用,性能并不理想,可以使用-XX:-UseBiasedLocking关闭偏向锁。
建议分别测量开启和关闭偏向锁情况下的应用性能,再决定是否关闭偏向锁优化。
(4).大页面内存支持:
计算机系统中的内存被划分成称为页的固定大小的块,程序访问内存的过程中会将虚拟内存地址转换为物理内存地址,这一转换过程通常是通过页表来完成的。遍历页表的代价通常比较高,为了减少每次内存访问时访问页表的代价,通常使用一块被称为转译快查缓存(TLB)的快速缓存对虚拟地址到物理地址的转换进行缓存。TLB通常只能容纳固定数量的条目,其每一条记录就是按页面大小统计的一块内存地址区间的映射,因此,系统的内存页面越大,每个条目能映射的内存地址区间越大,每个TLB能管理的空间也越大,地址转译请求在TLB中失效的可能性越小。当一个地址转译请求失效即无法在TLB中找到匹配项时,常常需要遍历内存中的页表查找虚拟地址到物理地址的映射,因此使用大页面内存的好处是减少了TLB失效的几率。
Linux和Windows操作系统需要做大页面相应的配置之后,使用-XX:+UseLargePages命令行选项可以开启HotSpot VM的大页面内存支持。