1.对Java程序进行调优的必要知识
Java程序在Java虚拟机中运行。因此为了进行调优,你需要理解JVM的工作流程。我之前有一篇博文Understanding JVM Internals,将让你对JVM有深入的了解。
本文中有关JVM运作过程的知识主要关于GC和Hotspot。尽管只有这两方面的知识可能无法对所有的Java程序进行调优,但是这两个因素在大多数情况下都影响着Java程序的性能。
值得注意的是,从操作系统的角度来看,JVM也是一个应用程序进程。为了给JVM创造良好的运行环境,你还需要对操作系统分配资源的过程有所了解。这意味着,想要调优Java程序,除了JVM你也应该理解操作系统或者硬件的工作方式。
需要具有的知识还有Java这门语言本身。另外理解锁和并发、类加载和对象创建都是非常重要的。
当开始调优Java程序时,你应该整合以上各方面的知识来完成工作。
2.Java程序性能调优的过程
图1是一张Java程序性能调优的流程图,摘自由Charlie Hunt和Binu John所著的Java Performance。
3.JVM调优过程
3.1.了解JVM分布式模型
JVM分布式模型用于决定是在一个JVM还是多个JVM上执行Java程序。你可以根据其有效性、响应能力和可维护性来进行选择。当在多台服务器上运行JVM时,你也可以选择将多个JVM运行于一台服务器或者每台服务器运行一个JVM。例如,对于每台服务器,你可以运行一个使用8GB堆内存的JVM,也可以运行4个使用2GB的JVM。你理应根据处理器内核的个数还有程序的特性来决定这个数量。当优先考虑响应能力时, 使用2GB的堆内存会优于8GB的,原因是这样能在更短的时间内完成Full GC。当然,8GB的堆内存可以降低Full GC的频率。如果你的程序使用了内部缓存,还可以通过增加缓存命中率来提高响应能力。综上所述,选择合适的模型需要考虑应用程序的特性,然后在各种模型中 选定一个能够扬长避短的。
3.2.了解JVM架构
选择JVM其实就是决定使用32位还是64位的JVM。在相同的条件下,你最好用32位的。因为32位的JVM比64位性能更好。然而,32位 JVM最大支持的堆内存是4GB(无论在32位操作系统还是64位的上,实际可分配的大小都只有2-3GB)。如果需要更大的堆内存,还是用64位的 JVM比较合适。
3.3.了解JVM参数
了解内容有:GC算法,以及表3中的内容
我会主要讲解如何为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. 记录内存转储文件后,为了管理的需要执行一个合适的操作。 |
4.监控jvm运行情况:
4.1 获取响应时间,并发量
4.2 获取GC时间,获取系统资源利用情况
5.切实地调优
5.1 stop-the-world耗时过长
stop-the-world耗时过长可能是由于GC参数不合理或者代码实现不正确。你可以通过分析工具或堆内存转储文件(Heap dump)来定位问题,比如检查堆内存中对象的类型和数量。如果在其中找到了很多不必要的对象,那么最好去改进代码。如果没有发现创建对象的过程中有特别的问题,那么最好单纯地修改GC参数。
为了适当地调整GC参数,你需要获取一段足够长时间的GC日志,还必须知道哪些情况会导致长时间的stop-the-world。想了解更多关于如何选择合适的GC参数,可以阅读我同事的一篇博文:How to Monitor Java Garbage Collection。
5.2 CPU使用率过低
当系统发生阻塞,吞吐量和CPU使用率都会降低。这可能是由于网络系统或者并发的问题。为了解决这个问题,你可以分析线程转储信息(Thread dump)或者使用分析工具。阅读这篇文章可以获得更多关于线程转储分析的知识:How to Analyze Java Thread Dumps。
你可以使用商业的分析工具对线程锁进行精确的分析,不过大部分时候,只需使用JVisualVM中的CPU分析器,就能获得足够的信息。
5.3 CPU使用率过高
如果吞吐量很低但是CPU使用率却很高,很可能是低效率代码导致的。这种情况下,你应该使用分析工具定位代码中性能的瓶颈。可使用的工具有:JVisualVM、Eclipse TPTP或者JProbe。

浙公网安备 33010602011771号