原文连接:http://www.cubrid.org/blog/dev-platform/the-principles-of-java-application-performance-tuning/java
本文是GC专家系列中的第五篇。在第一篇理解Java垃圾回收中咱们学习了几种不一样的GC算法的处理过程,GC的工做方式,新生代与老年代的区别。因此,你应该已经了解了JDK 7中的5种GC类型,以及每种GC对性能的影响。算法
在第二篇Java垃圾回收的监控中介绍了在真实场景中JVM是如何运行GC,如何监控GC数据以及有哪些工具可用来方便进行GC监控。segmentfault
在第三篇GC 调优中基于真实案例介绍了可用于GC调优的最佳选项。同时也描述了如何经过下降移动到老年代中对象的数量来缩短Full GC耗时,以及如何设置GC类型及内存大小。缓存
在第四篇Apache的MaxClients设置及其对Tomcat Full GC的影响中介绍了Apache对MaxClients
选项在系统发生GC时对总体性能的影响。性能优化
在本文中我将会介绍Java应用性能优化的通常原则。具体来讲,我会介绍性能优化的必要条件、判断是否须要优化的步骤,同时也会列出在性能优化过程当中经遇到的一些问题。在文章结尾,我会给你一些在性能优化过程当中如何作出最优决定的建议。服务器
不是每一个应用都须要优化。若是系统的运行情况正如你的指望,你就不必花费更多精力在额外的性能提高上。然而,在调试过程当中就指望系统能达到它的目标性能每每会比较困难。这时就须要作系统优化的工做了。无论使用哪一种语言,性能优化都要有较高的专业技能和高度专一。另外,由于每一个应用都有本身独特的操做和不一样的资源使用状况,在优化两个不一样系统中可能须要使用不一样的具体方法。因此与开发应用相比,性能优化更须要有扎实的基础知识,例如须要具备虚拟机、操做系统甚至计算机体系结构的相关知识。基于这些基础,再面对系统进行优化时,成功的机率就会更高。网络
一些Java应用的优化只须要调整JVM的选项,例如改变垃圾回收类型,不过有时也是须要去调整源码。无论使用哪一种方式,你首先都须要去监控Java应用的执行处理过程。基于此,本文主要涵盖的内容以下:并发
如何监控Java应用app
如何设置JVM选项高并发
如何判断是否有必要修改应用代码
Java应用在JVM中运行,所以优化Java应用,你须要理解JVM的运行过程。在前面的文章深刻理解JVM你能够找到一些关于JVM重要概念的介绍。
在本文中关于JVM运行过程的讲解着重于垃圾收集(GC)和 Hotspot相关知识。为了构造一个使JVM 运行良好的环境,你须要理解操做如何为进程分配资源。因此即使是优化Java应用,你也须要像熟悉JVM同样去熟悉操做系统甚至硬件知识。
与Java语言相关的知识也十分重要。一样理解锁和并发、熟悉类的加载与对象建立都是应该具有的技能。
一旦将Java应用优化付诸行动,你就须要综合利用上面提到的相关知识进行全面分析。
图1摘取自Charlie Hunt和Binu John合著的《Java性能》,描述了Java应用性能优化的处理流程。
图1: Java应用性能优化流程
上图并非一个一次性流程,在性能优化完成以前你可能须要重复其中的过程。此过程一样适用于如何选取一个指望的性能指标。在优化过程当中,有时须要下降性能指标的预期值,有时则须要提升性能指标的预期值。
JVM部署模型关系到如何决定是否把应用部署到单个或多个JVM上运行。这能够从系统的可用性、响应速度和可维护性上来作取舍。即使是决定了使用多个JVM,你也还须要肯定在单台服务器上运行多个JVM或者是每台服务器上运行一个JVM。例如,对每台服务器,你面临着为单个JVM分配8GB堆内存和运行4个JVM并为每一个JVM分配2GB堆内存的选择。固然单台服务器运行的JVM的数量也取决于CPU的核数以及应用自己的特色。在对比以上两个配置的响应速度时,具备2GB堆空间的方案可能更有优点,由于使用2GB的堆空间比使用8GB堆空间在Full GC时耗时更短。不过话说回来,使用8GB堆空间却能够减小Full GC的频率。另外也能够经过提升应用内部缓存命中率的方式来提升系统响应速度。因此,最终选择部署模型须要综合考虑应用的特色和所选方案对应用带来的优劣对比。
选择JVM时还须要面临32位JVM和64位JVM。一样条件下,应该优化选择32位JVM,由于32位JVM比64位的表现更优。不过32位JVM能使用堆内存最大理论值只有4GB。(事实上,32位操做系统和64位操做系统能分配的空间大小都只有2-3GB)。当堆空间需求更大时,使用64位JVM会是更好的选择。
表 1:性能对比
Benchmark | Time (sec) | Factor |
---|---|---|
C++ Opt | 23 | 1.0x |
C++ Dbg | 197 | 8.6x |
Java 64-bit | 134 | 5.8x |
Java 32-bit | 290 | 12.6x |
Java 32-bit GC* | 106 | 4.6x |
Java 32-bit SPEC GC* | 89 | 3.7x |
Scala | 82 | 3.6x |
Scala low-level* | 67 | 2.9x |
Scala low-level GC* | 58 | 2.5x |
Go 6g | 161 | 7.0x |
Go Pro* | 126 | 5.5x |
接下来要作的就是运行应用并衡量其性能。这些过程包括GC调优、调整操做系统设置以及修改应用代码。在这些过程当中,你须要使用一些系统监控工具或者程序分析工具来帮你完成任务。
值得注意的是为响应速度的优化和为吞吐量的优化途径可能会大相径庭。例如,不时发生的stop-the-world会下降响应速度,而Full GC则会致使单位时间内的吞吐量量大幅减小。因此其中一定会有所权衡。固然这些权衡不仅发生于响应速度和呑吐量之间,你可能须要使用更多的CPU资源来减小内存使用来以免响应速度或吞吐量的下降。与此相反的场景也一样会发生,你须要按必定的优先顺序来解决。
图1中的性能优化流程图适用于包括Swing应用在内的几乎全部Java应用。尽管如此,这个流程并不太适用于咱们NHN公司为网络服务编写服务器应用的场景。下图2是针对NHN公司并基于图1制定的一个简化的处理流程。
图2:NHN公司的推荐的Java应用优化过程
上图中的选择JVM(Select JVM)是说一般32位JVM就足够了,除非你须要使用JVM维护几个GB的缓存数据。
好了,基于图 2中的流程,你将开始学处处理每一步中所需应对的事情。
我将主要介绍如何为Web应用服务器设置合适的JVM参数。尽管不能穷尽全部案例,但最优的GC算法,尤为针对Web应用,一般是CMS GC,这主要是由于Web应用的低延迟要求决定的。固然在使用CMS过程当中,有时会遇到由于过多的内存碎片致使的较长时间的stop-the-world现象发生。不过这个问题能够经过调整新生代大小或者碎片比例进行优化。
设置新生代大小和设置整个堆大小同样重要。最好经过-XX:NewRatio
参数设置新生代空间与整个堆空间的大小比例,或者经过-XX:NewSize
来单独设置指望的新生代空间。设置新生代空间的重要性是由于大多数对象的存活时间很短。在Web应用中,除了缓存以外的大多数对象,是在与HttpRequest
相应的HttpResponse
建立的时候产生的,而这个过程不多会超过1秒,也就是说其中的对象的生命周期也不会超过1秒。若是新生代空间设置不够大,当须要建立新对象时,旧的对象就须要移到老年代。老年代的GC开销却比新生代GC开销大得多,所以设置恰当的新生代空间是十分重要的。
尽管如此,若是新生代空间超过必定比例,系统的影响速度将会下降。由于新生代垃圾回收的基本过程就把对象从一个存活区(Survivor area)复制到另一个存活区。因此像老年代同样,在新生代执行GC过程当中也一样会发生stop-the-world现象。若是新生代设置变大,存活区的空间相应也会增长,结果就是须要复制的数据空间将增长。基于这些特色,根据操做系统不一样,经过NewRatio
选项为HotSpot JVM设置合适的新生代空间是颇有必要的。
表2: 不一样操做系统与JVM选项的NewRatio默认值
OS and option | Default -XX:NewRatio |
---|---|
Sparc -server | 2 |
Sparc -client | 8 |
x86 -server | 8 |
x86 -client | 12 |
若是设置了NewRatio
,则将有1/(NewRatio + 1)
的堆空间属于新生代。你会发现上表中Sparc -server的NewRatio
的值很是小,由于当使用上面的默认值时,Sparc系统是用在比 x86更高端的场景中。由于x86性能的提高,目前使用x86 server也变得更为常见,像Sparc -server同样设置NewRatio
的值为2或3也更为合理。
除此以外,你也可使用NewSize
和MaxNewSize
做为NewRatio
的替代使用。新生代空间初始大小由NewSize
设定,而且随着内存消耗,新生代空间最大可扩展到MaxNewSize
的大小。随着NewRatio
的变化,Eden和Survivor区域的大小也在发生变化。正如经过相同-Xms
和-Xmx
为堆空间设置固定值,为新生代设置相同的MaxSize
和MaxNewSize
也是一个不错的选择。
若是同时设置了NewRatio
和NewSize
,其中较大的值会起做用。因此当一个堆空间建立以后,就能够经过以下公式计算初始新生代空间的大小:
min(MaxNewSize, max(NewSize, heap/(NewRatio + 1)))
不过在优化过程当中,无乎不可能一会儿就为堆大小和新生代大小找到了恰当的值。基于我在NHN运行Web应用程序的经验,我推荐在启动Java应用时使用以下JVM选项。在通过对这些选项的性能监控结果分析以后,你会找到更合适的GC算法或选项。
表3:推荐的JVM选项
选项类型 | 选项 |
---|---|
运行模式 | -server |
堆大小 | 指定相同的-Xms 和-Xmx |
新生代大小 | -XX:NewRatio : 取值在2-4之间 |
-XX:NewSize=? , -XX:MaxNewSize=? 。使用NewSize 替代NewRatio 也是不错的选择 |
|
永久代大小 | -XX:PermSize = 256m -XX:MaxPermSize=256m 把永久代大小设置为一个运行时不会出错的大小,由于它并不影响系统的性能 |
GC 日志 | -Xloggc:$CATALANA_HOME/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps 。输出GC日志并不明显影响应用性能,所以推荐保留详细的GC日志信息。 |
GC 算法 | -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 。这只是一个推荐的通用配置。根据应用特色不一样,其余配置也许更优。 |
OOM发生时输出堆dump | -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_HOME/logs |
OOM发生后的执行动做 | -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh 或者 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh 。OOM以后除了保留堆dump外,根据管理策略选择合适的运行脚本。 |
须要获取能反映应用性能的几个关键信息以下:
TPS(OPS):这个信息用于从概念上理解应用的性能。
Request Per Second(RPS):严格来讲,RPS并不一样于响应速度,但你能够把它理解为响应速度。经过RPS,你能够检查用户获取请求结果所耗费的时间。
RPS 标准误差(RPS Standard Deviation):若是有能够,尽可能保持RPS的稳定。若是出现误差,则须要检查是否须要作GC优化或者是否有内部系统问题。
为了获取尽量精确的性能结果,首先要对应用进行充分的预热,待稳定以后再开始性能测量,由于这时字节码已被HotSpot JIT进行了编译。一般,在使用nGrinder工具作负载测试时,至少要等系统达到某个负载水平10分钟后再测量系统的实际性能。
若是nGrinder的测试结果知足预期,那就不须要对应用进行优化。若是性能逊于预期,则须要开始优化以解决问题。下面经过具体案例来看性能优化的方法。
长时间的stop-the-world一般是因为使用了不恰当的GC选项或者不正确的应用实现所致。一般能够经过分析工具(profiler)或者堆dump的结果判断致使stop-the-world的缘由。也就是说能够经过检查堆中对象的类型和数量判断问题缘由。若是有过多非必须对象存在,则须要修改应用代码优化实现。若是在建立对象过程当中没有明显的问题,则须要调整GC选项。
为了把GC选项调整到恰当的设置,你须要有足够长时间的GC日志,并从中找出在哪一种情况下出现了stop-the-world。关于选择合适GC选项的具体细节,可参考Java 垃圾回收的监控。
当系统发生阻塞时,TPS和CPU使用率都会下降。问题可能来自于内部交互系统或者高并发。分析这种场景,能够对线程dump的结果进行分析或者使用分析工具(profiler)。线程dump的分析方法能够参考如何分析Java线程Dumps
使用一些商业分析工具(profiler),你能够获得很是具体的锁相关的分析报告。不过,大多数场景只须要使用jvisualvm中的CPU分析器就能够得到满意的结果。
若是TPS很低,但CPU使用率却很是高,就一般因为低效率的代码实现所致。这种场景,也须要经过使用分析器找到瓶颈的位置。可用的分析工具备jvisuavm,Eclipse的TPTP或者使用JProbe。
关于应用优化的一些建议途径以下:
首先,判断是否有必要作性能优化。衡量系统的性能并不是易事,任什么时候候都不能保证你能获得满意的结果。因此若是应用已经达到了指望的目标性能,就不必投入精力作额外的优化。
问题就在那里,你须要作的是解决它。Pareto 法则一样适用于性能优化。这并非说一个特定的低性能表现只来源于一个问题,相反,在性能优化过程当中,更应该把精力投入到对性能影响最大的那一点上。因此,当解决了最严重的问题后,就能够接着处理其余问题。不过建议是每次只着重解决一个问题。
你可能想到了气球效应。为了实现一个目标,你须要决定放弃哪些。你能够经过使用缓存来提升响应速度,然而随着缓存的增长,其Full GC所需耗时也将增长。通常来讲,若是你想维持少许的内存使用,系统的呑吐量和响应时间将会受到影响。因此,你要清楚哪些是最重要的,哪些微不足道的。
到目前为止,你已经了解了Java应用性能优化的方法。为了介绍衡量性能的具体过程,我忽略了一些细节。尽管如此,我想本文已足够应对Java Web应用的大多数优化场景。
做者:Se Hoon Park,网络平台开发实验室高级软件工程师,NHN公司