JAVA 调优

最佳实践
其实有很多简单的best practice可以在你真正调优之前就帮助很多。

使用最新版本的Java
每个主版本的Java虚拟机都包含新的功能,bugfix和性能的提升。在你开始调优工作之前,把你的Java升级到最新是很值得的。当然,花点时间了解最新版本的Java虚拟机的主要功能提升也是很有必要的。

诚然,不是所有的应用都能无缝地迁移到最新版本的虚拟机上,特别是一些第三方的ISV开发的应用。在这种情况下面,请把虚拟机升级到应用能够支持的最新版本(也请鼓励你的ISV考虑支持最新的Java特性)。


使用这个Java版本的最近更新
对于每个Java的主要release,比如J2SE 1.4.3, J2SE 5.0, Sun都会定期发布更新(Update)。比如对于J2SE 5.0, JavaSE 1.5.0_06就是一个Update。

Update发布通常包含Bugfix和性能提升。采用最新的Update,你就可以享受到最好的性能提升。

确保你的操作系统打了最新的Patch
虽然Java本身是跨平台的,它还是要依赖底层的操作系统来提供服务。所以保证底层操作系统是最新最稳定的也十分主要。

就那Solaris来说,关于运行Java应用就有一套关键的Patch。请参考OS厂商的文档来得到这些信息。

减少一切变数
请记住,你系统上的任何系统活动和应用都会潜在地影响到Java应用的性能测量和调优。这些因素会通过CPU,内存,磁盘,网络等方面的资源竞争来影响你的性能测量。


让数据来说话
理想的情况当然是测试一下某个改变前后的变化就能得出结论。问题是,有时候应用需要跑很长时间;有时候启动一个应用非常地复杂,依赖多个外部服务。然而,你就能想当然地从这些测试里下结论了吗?设计如何的测试计划都需要科学的方法。鉴于Java能够根据特定硬件环境和特定应用做自适应调整的特点,我们更加需要严肃的态度。任意细微的时间差异都可能导致性能测试过程中显著的结果变化。

注意微观层面的基准测量!
Java的一个优点之一是它能在运行时(run-time)做动态的优化。我们在越来越多的实例中发现,Java的性能可以超越静态编译的程序。然而,这一特性使得微观层面的基准测量变得十分困难。

Java性能之所以难以测量,其中一个原因就是它随之时间一直在变化。开始运行的时候,Java虚拟机往往要花些时间来“热身”。根据Java虚拟机不同的实现,它需要运行在“解释执行”模式下来发现那些“热点”方法。如果某个方法足够“热”,它会被编译和优化成本地(native)化的代码。

确实,有一些优化会铺开循环,把变量从循环内提取出,甚至舍弃“死”代码。你该怎么办呢?你难道要考虑所有机器的速度,然后调整循环的数目,使得你的应用在所有平台上都跑得差不多快吗?如果你试着这么去做的话,你的循环会在就是执行阶段被预估。然后优化之后跑得快很多,快到你观察到的时间其实大部分是虚拟机热身(warming-up)的时间,而不是你的循环。

对于某些应用,垃圾回收(GC)会把基准测量搞得更复杂。需要指出的是,对于特定的调优参数,垃圾回收的吞吐量是可以预估的。所以要么你避免把对象分配放在内嵌的循环中,要么跑足够长的时间来达到垃圾回收稳定状态。如果对象分配也是你基准测试的一部分,请小心地设计程序堆的大小,并且做足够多的采样,得到一个合理的均值来反映花在垃圾回收上面的时间。

关于基准测试还有更细微的陷阱。有没有考虑过如果你每次的循环花的时间不是固定值呢?举个例子,如果你的循环每次附加(append)一个字符串,那么由于每次附加之前要做一个字符串拷贝,所花的时间会越来越大。记得保证循环里的计算工作是恒定值而且是有意义的。

很显然跑到稳定状态才能保证得到可重复的结果。记得每次多跑几分钟。通常一分钟以内跑完的应用,Java虚拟机启动占了绝大多数时间。另一个关于时间的问题就是“抖动”。抖动往往发生在时序的粒度大大超过性能测量变化的本身。比方在某些Windows平台上,系统调用System.currentTimeMillis()只有15毫秒有效的粒度。对于比较短的测试,这会测试结果产生偏差。推荐所有新的测试使用System.nanoTime()方法来减少这种“抖动”。

使用统计方法

在实验之前,你应该尽可能减少影响性能的变数。但是,去除所有变数通常是不可能的,特别是那些由异步操作系统服务所带来的因素。通过重复多次实验然后去平均值,你的结果可以更接近“有用信号”而不是“噪音信号”。记住,信噪比和实验次数的平方根成正比。

考虑到Java性能测试,我们需要在更改之前测一下“基准”,更改之后测一下“样本”。比如说,对于某个性能测试,你测10次不带命令行参数的,再测10次带“-server”参数的。这就给了你两套不同的样本值。你关心的问题是,“加上这个参数有作用吗”,“如果有的话,作用有多大?”

第一个问题最重要,因为我们真正关心的是“作用足够明显,使得我们可以安全地下结论吗?”在统计学的行话里面,这可以表述成为“这两组样本足够反映真实数据吗?”根据样本个数,平均值,标准差和风险系数,我们可以计算出p-值或者概率值(probability)。通常来说,如果p-值在0.05以下的话,我们就认为这些Java参数的变化是“显著”的。

使用性能测试套件(Harness)

什么是性能测试套件?性能测试套件通常是一个脚本程序,用来启动测试,捕捉日志,抽取性能数据。通常这些套件能够按照规定次数来跑测试,自动根据不同的Java参数来计算统计数据。

有些性能测试本身包含测试套件。某些情况下你甚至可以重写脚本,测试更多的参数,捕获额外的数据,计算更多的统计数据。即使是一个很简单的脚本也能帮助你减少收集测试数据的单调乏味。它可以重复一致地启动应用,简化统计的计算流程。

无论你是否使用性能测试套件,很重要的一点是,你是否收集了足够的样本以至于可以安全地从这些结果里面得到结论。


调优的方法

这一节包含了你调优Java应用可以采用的不同选择。基于这些选择的比较应该采用我们刚才讨论的统计学方法来进行。


一般性的调优准则

这里是一些基本的调优准则,帮助你把不同的调优方法进行分类。

了解Java的动态调优机制

在你开始调优Java启动的命令行参数之前,请注意,Sun HotSpotTM Java虚拟机具备了调整自身的特性。这种智能的自我调整称为“Ergonomics”。大多数具备2颗CPU和2G以上物理内存的机器都可以被看成是服务器级别(server-class)的机器。这意味着一下选项被缺省打开:


-server 编译器
-XX:+UseParallelGC 并行的垃圾回收
-Xms 初始堆大小为机器物理内存的1/64
-Xmx 堆大小的上限是机器物理内存的1/4 (不超过1G)

请注意,32位的Windows系统都缺省使用-client编译器,64位系统如果满足上面标准的话被认为是server-class机器。



堆的大小调整


尽管“Ergonomics”机制大大提升了许多应用“开箱即得”的性能,我们仍然需要对Java内存大小调整有足够的重视。

一个Java应用程序可以使用的最大的堆的大小取决于下面三个因素:

进程数据模型(32-bit还是64-bit),以及相应的操作系统限制
系统可用的虚拟内存
系统可用的物理内存
一个特定Java应用程序的堆的大小不可能超过进程数据模型的最大虚拟内存限制。对于一个32位的进程模型,虚拟内存的地址大小是4G,然而有些操作系统会限制到2G或3G。典型的堆的最大设定值是:-Xmx3800m (1600m - 2G的情况),具体的限制和应用本身也有关。对于64位进程模型,最大值基本上可以认为没有限制。对于一个特定机器上的Java应用,Java堆的大小永远不能设成物理内存的大小,因为额外的内存需要保留给操作系统,其他进程,甚至其他的Java虚拟机使用。使用太多的系统内存很容易引起虚拟内存和磁盘之间的交换,特别是在垃圾回收的时候,导致严重的性能问题。在那些有多个Java应用的环境,或者多个应用的环境里,这些进程的堆的总和不应该超过系统物理内存的大小。

另一个非常重要的可调参数是Young Generation(也就是NewSize)的大小。通常来讲,Young Generation的最大值是堆大小的3/8。

垃圾回收策略
JavaTM平台提供了垃圾回收算法的选择。对于每一种算法存在有许多个可调参数。通常来说,下面的前两个是大型服务器应用最常用的选择:

-XX:+UseParallelGC 并行(吞吐)垃圾回收
-XX:+UseConcMarkSweepGC 并发(低暂停时间)垃圾回收
-XX:+UseSerialGC 串行垃圾回收(对小的应用和系统)

其它调优参数
通过适当地设置操作系统内存页面以及使用命令行参数-XX:+UseLargePages以及-XX:LargePageSizeInBytes,你可以从你的系统的内存管理系统中得到最好的效率。请注意,大的PageSize使我们能够更好地利用虚拟内存资源(TLB),但是这也使得Permanent Generation和Code Cache的尺寸变大,从而迫使你减小Java内存堆的大小。对于2MB或者4MB的内存页面问题或许不大,但是对于256MB的内存页,就值得仔细推敲了。


一个针对Solaris环境的例子就是选择libumem作为内存堆的分配器。为了体验libumem,你可以通过设置LD_PRELOAD环境变量来完成:

使一个shell中新的进程使用libumem
LD_PRELOAD=/usr/lib/libumem.so

在sh中使用libumem来启动Java程序
LD_PRELOAD=/usr/lib/libumem.so java java-settings application-args

在csh中使用libumem来启动Java程序
env LD_PRELOAD=/usr/lib/libumem.so java java-settings application-args

通过pldd或者pmap命令,你可以确定libumem是否被使用。


调优实例

普通
-Xmx256m -Xms256m -Xmn1024m -Xss128k

-Xms128M -Xmx512M -XX:PermSize=64M -XX:MaxPermSize=128M

高吞吐量

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20


并行的old generation收集
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC


使用256MB内存页

java -Xmx2506m -Xms2506m -Xmn1536m -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:LargePageSizeInBytes=256m


比较激进的优化

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:+AggressiveOpts


使用有偏向性的锁策略

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:+AggressiveOpts -XX:+UseBiasedLocking

低延迟和高吞吐
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:SurvivorRatio=8 -XX:TargetSurvivorRatio=90 -XX:MaxTenuringThreshold=31


监控和性能测量

讨论监控(抽取一个程序运行的粗略统计数据)和性能测量(借助工具获得程序运行性能的细节)是需要单独的白皮书来论述的。这里借助一些例子来说明Java性能调优时可以用到的工具。


监控
Java平台本身包含大量的监控工具,最流行的是JConsole和jvmstat


性能测量

Java平台本身包含一些性能测量工具,最流行的是-Xprof Profiler和HPROF Profiler。一个基于JFluid技术的profiler插件被集成���了NetBeans环境里面。


写高性能的代码

NIO(New I/O)API针对内存映射文件和可扩展的网络操作,提供了更好的性能。通过使用NIO,那些频繁使用内存或网络的应用程序将得到巨大的性能提升。一个很好的例子就是Glassfish当中的Grizzly Web Container。


另一个影响程序性能的Java新特性是Concurrency Utilities。越来越多的应用跑在了多CPU多核的服务器上。为了充分利用这一特性,程序必须设计成多线程的。传统的多线程编程架构过于复杂,线程之间的交互容易引起错误。有了Concurrency Utilities之后,开发人员就拥有了一整套设计模块。使用它们开发多线程应用程序将变得事半功倍。

你可能感兴趣的:(java,虚拟机,应用服务器,网络应用,J2SE)