陈同学
微服务
Accelerator
About
# Java应用性能优化之道 > 原文:[The Principles of Java Application Performance Tuning](https://www.cubrid.org/blog/the-principles-of-java-application-performance-tuning) BY **Se Hoon Park** ON 06/30/2017 > 翻译:[陈同学](https://chenyongjun.vip/) 本文是 **成为Java GC专家** 系列的第五篇。 第一篇 [理解 Java GC](https://chenyongjun.vip/articles/48) 中我们学习了不同GC算法的处理过程,GC是如何工作的,什么是年轻代和老年代,JDK7中的5种GC类型,以及每种GC类型对性能的影响。 第二篇 [如何监控Java GC](https://chenyongjun.vip/articles/54) 中讲述了运行中的JVM如何进行GC,如何监控GC以及一些高效监控GC的工具。 第三篇 [如何优化Java GC](https://chenyongjun.vip/articles/56) 中我们通过真实案例展示了一些你可以用得上的JVM调优参数。同时,也介绍了如何将传递到老年代的对象数量降到最少,如何减少 Full GC时间以及如何设置GC类型和内存大小。 > 第四篇笔者未做翻译,可参考 [MaxClients in Apache and its effect on Tomcat during Full GC ](https://www.cubrid.org/blog/maxclients-in-apache-and-its-effect-on-tomcat-during-full-gc/) 在本文中,我将介绍Java应用性能优化的基本原理。具体来说,我会介绍Java应用性能优化所需的内容,以及确定是否需要性能优化所需的步骤。我也会解释在性能优化过程中你可能遇到的问题。最后也会给出性能优化建议,以便做出更好的决策。 ## 概述 并非每个应用都需要优化,如果应用性能与预期一致,则无需花费额外的精力来提高其性能。但是,很难期望应用在完成调试后就立马达到目标性能。无论应用由何种语言开发,在其需要调优时,都需要很高的专业知识和专注力。此外,你也不会使用相同方法来调优不同的应用,因为每个应用都有不同操作以及不同资源的使用。因此,与开发应用相比,应用调优需要更多的基础知识,比方说,虚拟机、操作系统以及计算机架构等知识。 有时,Java应用调优只需要改变JVM参数,例如GC参数,但是有时也需要修改程序源码。无论使用何种方式,你首先都需要监控应用执行的过程,因此,本文主要处理以下几个问题: * **我该如何监控一个Java应用?** * **我该给什么样的JVM参数?** * **我怎么知道是否需要修改源码?** ## Java应用性能优化所需的知识 Java应用运行在JVM中,因此,你需要了解JVM的工作流程。你可以在我以前的一篇博文 [深入JVM内幕](https://chenyongjun.vip/articles/50) 中了解更多JVM的知识。 本文中关于JVM运行流程的知识主要指GC和HotSpot。虽然仅知晓GC或HotSpot的知识无法对所有Java应用进行调优,但大多数情况下,这两个因素会影响Java应用的性能。 值得注意的是,从操作系统角度来说,JVM也是一个应用进程。为了给JVM提供一个良好的运行环境,你需要了解操作系统如何为进程分配资源。这意味着,为了优化Java应用性能,你需要了解操作系统或硬件知识,以及JVM本身的知识。 另一方面,Java语言基础知识也非常重要。包含理解锁和并发的知识,同时也要熟练类加载和对象创建的知识。 当你进行Java应用性能优化时,你需要融会贯通这些知识。 ## Java应用性能优化的流程 下面的流程图来自于Charlie Hunt 和 Binu John联合撰写的《Java Performance》,这张图展示了Java应用性能优化的流程。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/07/16/abeea7ce3e204ca6beb60ce8665878aa.png) 上述过程不是一蹴而就的,你需要多次重复以上流程,直到完成优化。这个流程也适用于确定预期的性能指标。在优化过程中,有时你需要降低性能预期,有时却需要提高预期。 ### JVM分布模型(distribution model) JVM distribution model 与决定是在单个JVM上运行应用还是在多个JVM上运行多个应用相关。你可以根据可用性、响应性和可维护性来决定。当在多个服务器上运行JVM时,你还可以决定是在一台机器上运行多个JVM还是每台机器各运行一个JVM。例如,对于每台服务器,你可以决定使用8G堆内存运行一个JVM,或者运行四个JVM(每个JVM2G堆内存)。当然,你可以根据机器的核数以及应用的特征来决定在一个机器上运行几个JVM。 当以响应性为指标来比对两种配置时,应用使用2G内存可能比8G内存更有优势,因为使用2G内存时执行Full GC的时间更短。然而,如果你使用8G内存,你可以减少Full GC的频率。如果应用使用了内部缓存的话,你也可以通过提高命中率来提高响应速度。因此,你可以通过考虑应用的特性和方法来选择合适的模型,以克服选择的模型的缺点。 ### JVM架构(JVM architecture) 选择JVM意味着选择32位JVM还是64位JVM。在同等条件下,你最好选择32位的JVM,因为32位的JVM性能比64位更好。不过,32位JVM最大逻辑内存是4GB(但是32位和64位的操作系统实际分配的内存大小都是2-3G)。当需要的内存大于2-3G时,使用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、变更OS设置以及修改源码。对于这些,你可以使用系统监控工具或分析工具。 需要注意的是,响应性和吞吐量的优化方式是不同的。如果不断发生stop-the-world,响应性将大大降低,尽管吞吐量很高,但还是需要进行Full GC。你需要进行权衡,不仅仅是权衡响应性和吞吐量,你还需要考虑使用更多的CPU来降低内存使用,或者降低响应性和吞吐量的要求。你需要根据你的优先级来进行权衡。 上图的流程几乎展示了所有Java应用优化的流程,包含Swing应用。不过,这个图不太适合像我们公司为Internet服务编写的服务端程序。下面的一张图是基于图1设计的更简单的流程,耿适合我们NHN公司。 ![](https://blog-1256695615.cos.ap-shanghai.myqcloud.com/2018/07/16/a4d484e3e55a42f3b2d164f802bd89cf.png) 上面的 **Select JVM** 指尽可能使用32位的JVM,除非你需要用64位的JVM来维护几个G的缓存。 现在,基于图2,你可以了解每个步骤要做的操作。 ### JVM参数(JVM Options) 我将介绍如何为Web应用指定合适的JVM参数。尽管不一定适用于所有CASE,但对于Web应用来说最好的GC算法依然是 CMS GC,因为其延时很低。当然,使用CMS时,由于内存碎片的影响可能会发生很长时间的stop-the-world现象。不过,这个问题可以通过调整新生代大小(new area size)或内存碎片(fraction ratio)比率来解决。 指定新生代大小与指定整个堆区大小一样重要,你最好通过 **-XX:NewRatio** 指定新生代大小占整个堆区的比例,或者通过**-XX:NewSize**直接指定新生代的大小。指定新生代大小之所以很重要是因为大多数对象存活时间很短。在Web应用中,除缓存外的大多数对象都是在**HttpRequest** 和 **HttpResponse** 之间产生的,从请求开始到结束时间很难超过1秒,这意味着对象的存活时间也不会超过一秒。同样,如果新生代不够大,新创建的对象将被移动到老年代进行内存分配。老年代GC的成本远远高于新生代,因此,最好给新生代设置足够的空间。 如果新生代的大小超过一定水平,应用响应性将会降低。这是因为新生代的GC主要是将数据从一个survivor区拷贝到另一个 survivor区。另,在新生代或老年代执行GC时将会发生stop-the-world现象。如果新生代变大,survivor区也会增加,这样导致需要拷贝的数据量也会变多。鉴于此景,最好给新生代设置合适的大小。 *表2: NewRatio by OS and option* | 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 server,而且其性能也得到了提升。因此最好将NewRatio设置成 **Sparc -server**相似的的值(2或3)。 你也可以使用**NewSize** 和 **MaxNewSize** 来代替 **NewRatio**。通过NewSize指定新生代创建时的大小,最大值为MaxNewSize,Eden区和Survivor区的增长也取决于NewRatio比例。当你把 **-Xms** 和 **-Xmx** 设置成一样时,最好也把**MaxSize** 和 **MaxNewSize** 也设置成一样。 如果你同时指定了 **NewRatio**和**NewSize**,起作用的将是较大的值。因此,当堆创建之后,你可以通过以下公式计算新生代的初始大小: ``` min(MaxNewSize, max(NewSize, heap/(NewRatio+1))) ``` 然而,不太可能通过一次尝试就能决定合适的堆内存大小和新生代大小。根据我在NHN公司运行Web应用的经验,我建议用以下参数来运行应用。在监控应用性能之后,你可以使用更合适的GC算法或参数。 *表3: JVM参数推荐* | Type | Option | | --------------------------------------------- | ------------------------------------------------------------ | | Operation mode | `-sever` | | Entire heap size | `-Xms` 和 `-Xmx` 设置成相同值 | | New area size | `-XX:NewRatio`: 设置成2或4<br />`-XX:NewSize=?` `–XX:MaxNewSize=?`. 使用 `NewSize` 代替 `NewRatio` 更好 | | Perm size | `-XX:PermSize=256 m` `-XX:MaxPermSize=256 m`. 设置成可拓展的值,因为它不会影响性能,避免造成麻烦 | | GC log | `-Xloggc:$CATALINA_BASE/logs/gc.log` `-XX:+PrintGCDetails` `-XX:+PrintGCDateStamps`. 保留GC日志不会明显的影响Java应用的性能,最好尽可能的保留GC日志 | | GC algorithm | `-XX:+UseParNewGC` `-XX:+CMSParallelRemarkEnabled` `-XX:+UseConcMarkSweepGC``-XX:CMSInitiatingOccupancyFraction=75`. 这只是推荐的通用配置,不用特性的应用使用其他配置效果可能更好 | | Creating a heap dump when an OOM error occurs | `-XX:+HeapDumpOnOutOfMemoryError` `-XX:HeapDumpPath=$CATALINA_BASE/logs` | | Actions after an OOM occurs | `-XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh` or `-XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh` | ## 测量应用的性能 获取掌握应用性能的信息如下: * **TPS(OPS)**:从概念上了解应用性能的知识 * **Request Per Second(RPS)**:严格来说,RPS与响应性等同,但是你可以把它理解成响应性。通过RPS,你可以了解用户看到请求结果所花费的时间。 * **RPS标准误差**:如果可能的话,有必要记录RPS。如果发生偏差,你需要检查GC优化情况和网络情况。 为了获取更精准的性能结果,应该在充分预热应用后再进行测量,这是因为HotSpot JIT还需要编译字节码。通常,你可以使用nGrinder工具将负载提高到特定水平至少10分钟后再进行实际的性能测量。 ## 认真优化 如果nGrinder的执行结果符合预期,将没必要对应用进行性能优化。如果性能不及预期,你需要进行优化以解决问题。 ### stop-the-world事件耗时很长 不合适的GC选项可能导致stop-the-world时间过长,你可以通过分析器或heap dump来定位问题。这意味着你可以在检查堆中对象类型和数量后判断原因。如果你发现很多不必要的对象,你最好修改源码,如果创建对象过程中没有什么特别的问题,最好直接变更GC参数。 ### CPU使用率很低 当阻塞发生时,TPS和CPU使用率都会降低,这可能是因系统间调用或并发造成的。要分析这一点,你可以使用线程dump或分析器的结果进行分析。 你可以使用商业分析器对锁进行非常精准的分析。但是,在大多数情况下,通过jvisualvm中的CPU分析器你就能得到满意的结果。 ### CPU使用率很高 如果TPS很低但CPU使用率又很高,这可能是由于程序效率低下造成的。这种情况下,你可以使用分析器找到瓶颈所在。你可以使用jvivuavm、Eclipse的TPTP以及JProbe进行分析。 ## 优化的方法 建议您使用以下方法优化应用程序。 首先,你需要检查是否真的需要优化。性能测量过程不是一件简单的事情,而且也无法保证总是得到满意的结果。因此,如果应用性能符合预期,就不要花费额外的精力来进行优化。 问题往往在一个地方,你只需要解决它即可。帕累托原则也适用于性能优化,它强调我们在性能调优时应用关注影响最大的因素。因此,最好在修复最重要的问题后再着手处理其他事情,建议你最好每次只解决一个问题。 你也应该考虑气球效应,要有所取舍。你可以通过使用缓存来提高响应性,但是缓存一旦增加,Full GC的时间也会变长。通常,如果你想更少的使用内存,吞吐量和响应性就会降低。因此,你需要拿捏好孰轻孰重。 到目前为止,你已经了解了Java应用性能优化的方法。为了介绍性能优化的过程,我不得不省略一些细节。不过,我认为这已经可以满足Java应用性能调优的大多数情况。 祝你好运!
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
Java应用性能优化之道
文章链接:
https://chenyongjun.vip/articles/60
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力