Java应用程序性能调优的原则
这是“ 成为Java GC专家 “系列中的第五篇。 在第一个问题探讨理解Java垃圾收集里,我们已经学会了不同的GC算法的过程,GC如何的工作,新生代和老一代是什么,你应该知道新的JDK 7的5种类型的GC,以及这些GC类型性能的影响有什么。
在第二篇文章-如何监视 Java 垃圾收集中解释了JVM如何实时地运行垃圾收集,我们如何监控GC,和我们可以用来使这个过程更快速,更有效的工具。
在第三篇文章-如何调整Java的垃圾回收中,我们演示了基于实际例子使用GC调整的最佳选项。 此外,我们解释了如何最大限度地减少传递给老区对象的数量,降低Full GC的时间,以及如何设置GC类型和内存大小。
在第四篇文章-Apache的MaxClients和Tomcat的全GC 的影响中,我们已经解释了Apache中MaxClients参数的重要性, GC发生时,显着地影响到整个系统的性能。
在这第五篇文章中,我将解释Java应用程序性能调优的原则。 具体来说,我将解释需要以怎样的顺序调整Java应用程序的性能,确定是否需要调整您的应用程序执行的步骤。 我也解释了性能优化过程中可能遇到的问题。 本文将帮助你做出更好的决策,并给出优化Java应用程序时需要遵循的建议。
概述
并不是每一个应用程序都需要调整。 如果应用程序如预期那样执行,您不必施加额外的努力,以提高其性能。 然而,很难期望应用程序能达到完成开发调试时的性能。 这正是需要调优的时候。 无论实现语言是什么,调优应用程序需要很高的专业知识和专心。 此外,您可能不能使用相同的方法像调整某个应用程序一样调整其他应用程序。 这是因为每个应用程序有其独特的操作和不同类型的资源使用情况。 出于这个原因,调整应用程序和编写应用程序所需要的知识相比,需要更多的基本知识。 例如,你需要这些知识:虚拟机,操作系统和计算机体系结构。 当你基于这样的知识专注于应用程序领域,才可以成功地调优应用程序。
有时Java应用程序优化只需要改变JVM选项,如垃圾收集 ,但有时它需要改变应用程序的源代码。 无论你选择哪种方法,你需要首先监视Java应用程序的执行过程。 出于这个原因,本文将处理的问题如下:
- 我怎么监视Java应用程序?
- 我应该做什么样的JVM选项修改?
- 我怎么能知道,是否需要修改源代码?
调整Java应用程序性能所需的知识
Java应用程序运行在Java虚拟机(JVM)。 因此,要优化Java应用程序,你需要了解JVM运行过程。 我曾撰写博客了解JVM内部,那里你可以找到关于JVM的很棒的建议。
这篇文章里提到的关于JVM执行过程的知识主要有GC垃圾回收的知识和Hotspot。 虽然你可能无法用GC或Hotspot知识调整各种Java应用程序性能,但这两个因素在大多数情况下影响着Java应用程序的性能。
值得注意的是,从操作系统的角度看JVM也是一个应用程序进程。 为了能使JVM可以在良好的环境运行,你应该明白OS如何分配资源给进程。 这意味着,调整Java应用程序的性能,你应该有操作系统或硬件以及JVM本身知识。
另一个方面是,Java语言领域的知识也很重要。 同样重要的是要了解锁或并发性和熟悉类加载或创建对象。
当你进行Java应用程序性能调优,你应该运用所有这些方面的知识。
Java应用程序性能调优的过程
图1显示了一个流程图,来自由查理·亨特和BinU 约翰共同撰写的书<Java性能>。 这个图表显示了Java应用程序性能调优的过程。
图1:优化Java应用程序性能流程。
上述过程不是一个时间过程。 您可能需要重复,直到完成调优。 这也适用于确定一个预期的性能值。 在调整的过程中,有时你应该降低预期的表现值,有时要提高。
JVM分配模型
JVM分配模型关系到是否在一个单独的JVM还是在多个JVM上运行Java应用程序。 你可以根据可用性,响应性和可维护性来决定它。 在多台服务器上运行JVM时,您也可以决定是否要在单个服务器上运行多个JVM或每台服务器上运行一个JVM。 例如,对于每个服务器,您可以决定是否执行一个单独使用8 GB堆的JVM,或使用2 GB堆的JVM。 当然,你可以根据CPU内核数量和应用程序的特点,来决定一台服务器上运行的JVM的数量。 在响应方面比较这两个设置时,用2GB比8GB内存可能会更有利应用程序的响应性,使用2 GB的堆进行一次完全的垃圾收集所需时间较短。 但是,如果您使用8 GB堆,可以减少Full GC的频率。 您还可以提高响应速度,增加命中率,如果应用程序使用的内部高速缓存的话。 因此,你可以选择合适的发布模式,考虑应用程序的特点和克服因某些优势而选择模型缺点的方法。
JVM体系结构
选择一个JVM是否要使用32位JVM或64位JVM。 在相同的条件下,你最好选择一个32位的JVM。 这是因为一个32位JVM的性能比一个64位的JVM好。 然而,32位JVM的最大逻辑堆的大小是4 GB。 (但是,32位操作系统和64位操作系统实际分配的大小为是2-3 GB)。当需要比这更大的内存时可适当使用64位JVM。
Table 1: 性能比较 (source).
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的垃圾收集,尽管有大量的每单位时间的吞吐量。 您还需要考虑trade-off可能发生, 在响应速度和吞吐量之间做权衡。 您可能需要使用更多的CPU资源,以减少内存使用量或减少了反应能力或吞吐量。 相反的情况也同样发生,你需要根据优先级处理它。
上面的图1的流程图显示了几乎所有类型的Java应用程序,包括Swing应用程序的性能优化方法。 然而,这张图对编写服务器应用程序的互联网服务有点不适合,像我们公司NHN。 下面的图2中的流程图是一个简单的流程, 在图1的基础上设计的,是更适合NHN。
图2:调整NHN的Java应用程序的推荐过程。
在上面的流程图中选择“JVM”是指尽可能使用一个32位的JVM除非你需要使用64位的JVM,以维持几个GB的高速缓存。
现在, 在图2流程图的基础上,你将了解做执行每个步骤要做的事情。
JVM选项
我将解释如何指定合适的JVM选项,这主要是针对Web应用程序服务器。 有一种最好的GC算法,尽管没有被应用到每种场景,尤其是Web服务器应用程序,叫并发标记清除GC(Concurrent Mark Sweep GC) 。 这因为最重要的是低延迟 。 当然,在使用Concurrent Mark Sweep GC,有时很长的stop-the-worl现象可能发生。 然而,这个问题是通过调整的新的区域new area的大小或比例可能得到解决。
指定新的区域new area的大小和指定整个堆大小是一样重要的。 你最好通过使用–XX:NewRatio指定新的区域大小对整个堆大小的比例,或通过使用–XX:NewSize选项,指定所需的新的区域大小。 指定一个新的区域的大小是很重要的,因为大多数的对象不能长期生存。 在Web应用程序中,大多数对象,除了缓存数据,是当HttpResponse到HttpRequest创建时产生的 。 这个时间几乎不超过一秒钟。 这意味着对象的寿命不超过一秒钟。 如果新的区域规模并不大,它应该被转移到老区,以腾出空间给新创建的对象。 GC老区的成本是远远大于新区域的,因此,最好设置新的区域的大小足够。
如果新区域的大小超过一定的水平,但是响应速度将降低。 这是因为垃圾收集新领域基本上是从一个残留区域的数据复制到另一个残留区。 此时stop-the-world现象会发生,不管是在执行GC新区域还是老区。 如果新的面积变得更大,残留区域的大小将增加,并因此要复制数据的大小也将增加。 鉴于这样的特点,最好通过指定OS HotSpot JVM 的NewRatio设置一个合适的新区域大小 。
Table 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服务器,其性能也得到提高。 因此,最好是2或3,这个值和 Sparc 服务器类似。
您也可以指定NewSize和MaxNewSize ,而不是NewRatio 。 指定NewSize的值并指定MaxNewSize为最大增量。 根据(指定的或默认值)比例Eden或Survivor残留区域也增加了。 像指定-Xms -Xmx相同大小一样, 最好指定MaxSize和MaxNewSize为相同的大小。
如果您已指定NewRatio和NewSize ,你应该使用较大的一个。 因此,当堆已经创建,您可以表示初始新区域的大小,如下:
min(MaxNewSize, max(NewSize, heap/(NewRatio+1)))
然而,不可能一次测试能确定适当的整个堆大小和新的区域的大小。 根据NHN Web服务器应用程序运行的经验,我建议用下面的JVM选项运行Java应用程序。 使用这些选项监控应用程序的性能后,您可以使用一个更合适的GC算法或选项。
Table 3: Recommended JVM options.
Type | Option |
---|---|
Operation mode | -sever |
Entire heap size | 指定-Xms和-Xmx为相同的值. |
New area size | -XX:NewRatio : value of 2 to 4 |
-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 |
发生OOM后的操作 | -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh or -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh . 生成 heap dump 后, 根据管理策略采取适当的操作. |
测量应用程序的性能
要获取的应用程序性能的信息如下:
- TPS(OPS):了解应用程序性能所需要的信息。
- 每秒请求(RPS):严格来说,RPS难以感受到,但你可以把它理解为响应能力。 通过RPS,您可以检查用户看到结果所花费的时间。
- RPS的标准偏差:可能的话有必要达到RPS要求。 如果出现偏差,您需要检查GC调整或互通系统。
为了获得更准确的性能测试结果,你应该应用程序充分预热后才衡量它。 这是因为预期的HotSpot JIT编译字节代码。 在一般情况下,你可以使用nGrinder负载测试工具进行负载测试某一特征进行至少10分钟后才能得到的实际性能。
认真的调整
如果执行nGrinder的结果满足期望,你并不需要调整应用程序的性能。 如果效果没有达到预期,就需要进行调整来解决问题。 下面,您将看到各种案例的处理方法。
Stop-the-World事件持续很长
长时间Stop-the-World,可能由于不恰当的GC选项或不正确实施导致。 您可以根据探查器profiler 结果或堆转储heap dump来判定原因。 这意味着你可以检查堆中对象的类型和数量后判断原因。 如果您发现了很多不必要的对象,你最好修改源代码。 如果你发现在创建对象的过程中,没有特别的问题,你只要改变GC的选项来调优。
要适当调整GC的选项,你需要确保有一个足够长时间的GC日志。 您需要了解在什么情况下Stop-the-World需要很长的时间。 选择适当的GC选项的更多信息,请阅读有关如何监视Java垃圾收集博客。
阻塞事件时CPU使用率低
当发生阻塞时,TPS和CPU使用率都会下降。 这可能会导致互通系统或并发的问题。 要分析这个问题,你可以使用线程转储thread dump或分析器profiler分析的结果。 线程转储thread dump分析的更多信息,请参阅如何分析Java线程堆 。
您可以使用商业profiler分析器进行非常精确锁lock分析。 然而,在大多数情况下,你可以用jvisualvm的CPU分析器得到一个满意的结果。
CPU使用率高
如果TPS是低的,但CPU的使用率是很高的,这很可能是由于低效率的编码实现导致。 在这种情况下,你应该使用分析器找出瓶颈的位置。 您可以通过使用jvisuavm,Eclipse的TPTP 或JProbe进行分析。
调优方法
建议您使用下面的方法来调整应用程序。
首先,你应该检查是否性能调整是必要的。 性能测量是不容易的工作。 你也不能保证所有的时间都能得到满意的结果。 因此,如果应用程序已经达到其目标性能,你就不必额外投入精力。
问题出在那么少数几个地方,所有您需要做的是解决它。 帕累托原则也适用于性能调整。 这并不意味着低性能的某个功能一定是由一个单一的问题导致的。 相反,在性能调优时我们应该专注于性能影响最大的一个因素。 因此,你应该修复最重要那个问题后接下来处理其他问题。 建议您尝试一次解决一个问题。
你应该考虑气球效应 。 您应该判断什么该放弃什么要抓住。 你可以使用应用缓存提高反应速度,但如果增加高速缓存的大小,进行全面的Full GC所花费的时间也将随之提高。 在一般情况下,如果你想有一个小的内存使用量,吞吐量或响应可能会恶化。 因此,你需要考虑什么是最重要的,什么是不那么重要。
到目前为止,您已经阅读了Java应用程序性能调优的方法。 在介绍具体的性能测量过程中,我不得不省略一些细节。 不过,我认为,这应该满足大多数优化Java Web服务器应用程序的情况。
祝你性能优化成功!
硒勋公园 ,高级软件工程师,网络平台开发实验室,NHN公司。
一次Java垃圾收集调优实战 - 企业应用 - Java - ITeye论坛
Parallel收集高达1秒的暂停时间基本不可忍受,所以选择CMS收集器。
在被压测的Mule 2.0应用里,每秒都有大约400M的海量短命对象产生:
- 因为默认60M的新生代太小了,频繁发生minor gc,大约0.2秒就进行一次。
- 因为CMS收集器中MaxTenuringThreshold(生代对象撑过过多少次minor gc才进入年老代的设置)默认0,存活的临时对象不经过Survivor区直接进入年老代,不久就占满年老代发生full gc。
对这两个参数的调优,既要改善上面两种情况,又要避免新生代过大,复制次数过多造成minor gc的暂停时间过长。
- 使用-Xmn调到1/3 总内存。观察后设置-Xmn500M,新生代实际约460m。(用-XX:NewRatio设置无效,只能用 -Xmn)。
- 添加-XX:+PrintTenuringDistribution 参数观察各个Age的对象总大小,观察后设置-XX:MaxTenuringThreshold=5。
优化后,大约1.1秒才发生一次minor gc,且速度依然保持在15-20ms之间。同时年老代的增长速度大大减缓,很久才发生一次full gc,
参数定稿:
-server -Xms1024m -Xmx1024m -Xmn500m -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=5 -XX:+ExplicitGCInvokesConcurrent