这是“成为Java GC专家”系列的第五篇文章。在第一篇深刻浅出Java垃圾回收机制中,咱们已经学习了不一样的GC算法流程、GC的工做原理、新生代(Young Generation)和老年代(Old Generation)的概念。你应该了解了JDK7中5种GC类型以及各类类型对应用程序的影响。 html
在第二篇如何监控Java的垃圾回收中,阐述了JVM是怎样实际执行垃圾回收的,咱们怎样去监控GC以及哪些工具能让这个过程更高效。 java
第三篇如何如何优化Java垃圾回收机制中展现了一些基于真实案例的最佳实践。同时讲解了怎样尽可能少地将对象放入老年代空间(Old Area),避免频繁地执行彻底垃圾回收(Full GC)。还说明了如何设置GC的类型和内存大小。 算法
在第四篇Apache的MaxClients参数详解及其在Tomcat执行FullGC时的影响中,解释了MaxClients参数的重要性以及它在垃圾回收过程当中对整个系统性能的显著影响。 编程
第五篇文章将讲解Java程序性能调优的原则,尤为是在这个过程当中必要的知识以及判断你的程序是否须要调优。还会介绍调优过程当中你可能遇到的问题。本文最后会给出一些建议,依据这些你能在对Java程序调优时作出更好的决策。 缓存
并非每一个程序都须要调优。若是一个程序性能表现和预期同样,你没必要付出额外的精力去提升它的性能。然而,在程序调试完成以后,很难立刻就知足它的性能需求,因而就有了调优这项工做。不管哪一种编程语言,对应用程序进行调优都须要丰富的技术知识而且注意力高度集中。另外,你也不该该用相同的方式对两个程序调优,由于每一个程序都有它本身独特的运做方式和不一样的资源使用方式。正因如此,调优比写程序须要更多基础知识。例如,你须要熟悉虚拟机、操做系统和计算机架构。而当你面对在这些知识基础上编写的程序时,就能成功地对它进行调优。 服务器
有时调优Java程序只须要修改JVM参数,好比GC的参数。但也有些时候须要修改程序代码。不管那种方法,你首先都须要监控执行Java程序的进程。所以本文会讲解下面几个问题: 网络
Java程序在Java虚拟机中运行。所以为了进行调优,你须要理解JVM的工做流程。我以前有一篇博文Understanding JVM Internals,将让你对JVM有深刻的了解。 架构
本文中有关JVM运做过程的知识主要关于GC和Hotspot。尽管只有这两方面的知识可能没法对全部的Java程序进行调优,可是这两个因素在大多数状况下都影响着Java程序的性能。 并发
值得注意的是,从操做系统的角度来看,JVM也是一个应用程序进程。为了给JVM创造良好的运行环境,你还须要对操做系统分配资源的过程有所了解。这意味着,想要调优Java程序,除了JVM你也应该理解操做系统或者硬件的工做方式。 app
须要具备的知识还有Java这门语言自己。另外理解锁和并发、类加载和对象建立都是很是重要的。
当开始调优Java程序时,你应该整合以上各方面的知识来完成工做。
图1是一张Java程序性能调优的流程图,摘自由Charlie Hunt和Binu John所著的Java Performance。
JVM分布式模型用于决定是在一个JVM仍是多个JVM上执行Java程序。你能够根据其有效性、响应能力和可维护性来进行选择。当在多台服务器上运行JVM时,你也能够选择将多个JVM运行于一台服务器或者每台服务器运行一个JVM。例如,对于每台服务器,你能够运行一个使用8GB堆内存的JVM,也能够运行4个使用2GB的JVM。你理应根据处理器内核的个数还有程序的特性来决定这个数量。当优先考虑响应能力时, 使用2GB的堆内存会优于8GB的,缘由是这样能在更短的时间内完成Full GC。固然,8GB的堆内存能够下降Full GC的频率。若是你的程序使用了内部缓存,还能够经过增长缓存命中率来提升响应能力。综上所述,选择合适的模型须要考虑应用程序的特性,而后在各类模型中 选定一个可以扬长避短的。
选择JVM其实就是决定使用32位仍是64位的JVM。在相同的条件下,你最好用32位的。由于32位的JVM比64位性能更好。然而,32位 JVM最大支持的堆内存是4GB(不管在32位操做系统仍是64位的上,实际可分配的大小都只有2-3GB)。若是须要更大的堆内存,仍是用64位的 JVM比较合适。
表1:性能比较(数据来源)
测试基准 | 时间(秒) | 系数 |
---|---|---|
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-word(串行GC暂时中断程序执行),程序的响应能力就会被下降。好比在高吞吐量时执行Full GC。不要忘记,在调优时每每有得有失。这样须要折衷处理的事情不只发生在响应能力和吞吐量之间。例如使用更多的CPU资源来下降内存的使用,或者不得不忍受响应能力和吞吐量其中一个性能指标的降低。相反的状况一样可能发生,实际的调优应该根据各指标的优先级来执行。
上面图1中的流程展现了几乎可用于全部Java程序的性能调优过程,包括Swing应用。然而,对于咱们公司NHN用于提供网络服务的服务器端程序来讲,这个方法多少有些不合适。下面图2中的流程是根据图1修改而来,它更简单,也更适合NHN。
其中,Select JVM表示尽量使用32位的JVM,除非你须要用64位的JVM来维护一个数GB的缓存。
如今,跟随图2中的流程,你会了解到每一步具体的工做。
我会主要讲解如何为Web服务端程序设置合适的JVM参数。尽管不必定适合全部的案例,可是最好的GC算法是Concurrent Mark Sweep(CMS垃圾回收),特别是对于Web服务端程序。由于低延迟是很是重要的。固然,在使用CMS时,因为新生代空间(New Area)的分配,可能发生较长时间的stop-the-world现象,不过调整新生代空间的大小或者它和整个堆空间的比例可能解决这个问题。
指定新生代空间的大小和指定整个对堆内存的大小一样重要。你最好使用–XX:NewRatio来指定新生代和整个堆的大小比例,或者直接用–XX:NewSize来指定所需的新生代空间。这个配置是很是必要的,由于大部分对象都不会存活好久。在Web程序中,除了缓存数据,其余多数对象都只在HttpRequest到HttpResponse期间建立。这个时间几乎不会超过1秒,表示这些对象的存活时间也不会超过1秒。若是新生代空间不够大,对象会被转移到老年代空间,以便腾出地方给新对象使用。老年代空间(Old Area)垃圾回收的代价是比新生代空间大的多的,所以很须要设置一个充足的新生代空间。
然而,当新生代空间的大小超过一个特定的水平,程序的响应能力会被下降。由于新生代空间的垃圾回收过程,基本上是将数据从一个Survivor Area复制到另一个(From Space和To Space)。另外,stop-the-world的现象在新生代空间和老年代空间执行垃圾回收时都会发生。若是新生代空间变大,那么Survivor Area的空间也会更大,因而每次复制的数据就更多。基于这样一种特性,咱们应该经过指定不一样操做系统中HotSpot JVM的NewRatio参数来分配合适大小的新生代空间。
表2:不一样操做系统和配置下NewRatio的默认值
操做系统及参数 | 默认-XX:NewRatio |
---|---|
Sparc -server | 2 |
Sparc -client | 8 |
x86 -server | 8 |
x86 -client | 12 |
若是设置了NewRatio,那么整个堆空间的1/(NewRatio +1)就是新生代空间的大小。上表能够看出Sparc -server的NewRatio默认值很小,由于相比x86的操做系统,Sparc之前更多用于高端应用,这个值就是为它们设置的。但如今x86操做系统的性能有很大提高,使用它们做为服务器已经很广泛了。所以指定NewRatio为2或者3是更好的选择,就和Sparc -server上的配置同样。
另外,你还能够经过指定NewSize和MaxNewSize来代替NewRatio。那么新生代空间建立时的大小就是指定的NewSize,随后能够一直增加到MaxNewSize的值。Eden(新建立对象存放的区域)和Survivor Area两个区域会随比例增长。就和你为-Xms(译者注:原文是-Xs,应该是笔误)和-Xmx设置相同的值同样,将MaxSize和 MaxNewSize设置为相同的也是一个好选择。
若是同时指定了NewRatio和NewSize,你应该使用更大的那个。因而,当堆空间被建立时,你能够用过下面的表达式计算初始新生代空间的大小:
1
|
min(MaxNewSize, max(NewSize, heap/(NewRatio+1)))
|
不管如何,仅经过一次尝试就找到合适的堆空间和新生代空间大小是不可能的。根据我在NHN运行Web服务器的经验,建议使用下面的JVM参数来运行Java程序。监控在这些参数的条件下程序的性能表现以后,你就可以选择更合适的GC算法或者配置。
表3:推荐的JVM参数
类型 | 参数 |
---|---|
运行模式 | -sever |
整个堆内存大小 | 为-Xms和-Xmx设置相同的值。 |
新生代空间大小 | -XX:NewRatio: 2到4. -XX:NewSize=? –XX:MaxNewSize=?. 使用NewSize代替NewRatio也是能够的。 |
持久代空间大小 | -XX:PermSize=256m -XX:MaxPermSize=256m. 设置一个在运行中不会出现问题的值便可,这个参数不影响性能。 |
GC日志 | -Xloggc:$CATALINA_BASE/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps. 记录GC日志并不会特别地影响Java程序性能,推荐你尽量记录日志。 |
GC算法 | -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75. 通常来讲推荐使用这些配置,可是根据程序不一样的特性,其余的也有可能更好。 |
发生OOM时建立堆内存转储文件 | -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_BASE/logs |
发生OOM后的操做 | -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh 或 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh. 记录内存转储文件后,为了管理的须要执行一个合适的操做。 |
为了获得程序的性能表现,须要如下这些信息:
为了获得更准确的性能表现,你应该等到程序完全启动完成后再进行测量,由于字节码随后会被HotSpot JIT编译为本地机器码。整体来讲,须要在程序加载完指定功能后,用nGrinder等工具测试至少10分钟。
若是nGrinder测试的结果知足了预期,那么你不须要对程序进行性能调优。若是没有达到预期结果,你就应该执行调优来解决问题。接下来会经过实例讲解方法。
stop-the-world耗时过长多是因为GC参数不合理或者代码实现不正确。你能够经过分析工具或堆内存转储文件(Heap dump)来定位问题,好比检查堆内存中对象的类型和数量。若是在其中找到了不少没必要要的对象,那么最好去改进代码。若是没有发现建立对象的过程当中有特别的问题,那么最好单纯地修改GC参数。
为了适当地调整GC参数,你须要获取一段足够长时间的GC日志,还必须知道哪些状况会致使长时间的stop-the-world。想了解更多关于如何选择合适的GC参数,能够阅读我同事的一篇博文:How to Monitor Java Garbage Collection。
当系统发生阻塞,吞吐量和CPU使用率都会下降。这多是因为网络系统或者并发的问题。为了解决这个问题,你能够分析线程转储信息(Thread dump)或者使用分析工具。阅读这篇文章能够得到更多关于线程转储分析的知识:How to Analyze Java Thread Dumps。
你可使用商业的分析工具对线程锁进行精确的分析,不过大部分时候,只需使用JVisualVM中的CPU分析器,就能得到足够的信息。
若是吞吐量很低可是CPU使用率却很高,极可能是低效率代码致使的。这种状况下,你应该使用分析工具定位代码中性能的瓶颈。可以使用的工具备:JVisualVM、Eclipse TPTP或者JProbe。
建议你使用以下方法对程序进行调优。
首先,检查性能调优是否必要。测量性能不是一件简单的工做,你也不能保证每次都得到满意的结果。所以若是程序已经知足预期性能需求,没必要在调优上增长额外的投入了。
问题只出在一个地方,你要作的就是去解决掉它。二八定律(Pareto principle)对性能调优一样适用。这不是说某个模块的低性能必定只源于一个问题,而是强调咱们应该在调优时把注意力放在影响最大的那个问题上。在处理好了最重要的以后,你才应该去解决剩下其余的。也就是建议一次只对一个问题进行修复。
另外须要考虑到气球效应(Balloon effect),有得必有失。你能够经过使用缓存来提升响应能力,可是当缓存逐渐增大,执行一次Full GC的时间也会更长。通常而言,若是你但愿内存使用率比较低,那么吞吐量和响应能力可能都会恶化。所以,要知道什么对本身程序来讲最重要的,而哪些又是次要的。
到此为止,你应该已经了解了如何对Java程序进行性能调优。为了介绍性能测定的具体过程,我不得不省略其中一些细节,不过我认为这些也足够应对大多数Java Web服务端程序了。
最后祝调优好运!