Java JVM性能调优常见问题解答
本文主要介绍Java HotSpot VM相关的一些常见问题,以及相关性能调优的问题。除非特别说明,否则说的所有信息适用于1.4以上版本的HotSpot Client VM跟HotSpot Server VM。
本文的内容主要来自于Java的官方文档:www.oracle.com/technetwork/java/hotspotfaq-138619.html
按照文档中分类,主要分为下面几类问题,一般问题,内存与垃圾回收问题,JIT编译问题,64位Java问题,多线程应用问题,性能问题和Java HotSpot基准测试。本文选择其中具有代表性的问题进行说明。用于指导Java程序的开发与Java JVM调优与性能检测。
一般性问题
1. 我想获取程序运行情况分析,我应该怎么办?
可以使用-agentlib:hprof打印profiling信息,可以通过使用-agentlib:hprof=help来查看不同种类的分析数据。
2. 我持续运行使用的文件句柄数超过了上限该怎么办?
对于某些应用可能需要使用大量的文件句柄,对于如socket连接等都会占用句柄数,如果需要处理大量连接,则可能导致文件句柄数超限。你可以做的唯一的方法就是调高系统的文件句柄数。对于Linux系统,可以通过ulimit -n查看可以使用的文件句柄数,可以使用ulimit -n 2048直接修改最大句柄数,但是这种设置方式只对当前shell有效,如果重新打开一个则会失效。对于系统级修改则需要修改系统参数,vi /etc/security/limits.conf修改
|
|
修改保存以后,注销当前用户重新登录就生效了。
内存与垃圾回收问题
1. 程序垃圾回收执行pause时间超长,什么参数能优化这种情况?
这种情况下有几种事情可以做,首先,可以试一下-Xincgc
参数。这个参数使JVM使用增量垃圾回收算法。这样一次回收一小部分的堆内存空间而不是整个堆。对于大多数程序来说,这样可以降低pause时间,但是有的情况下可能更糟。
其次,你可以降低你的堆内存使用。堆内存过大会引起垃圾回收期停顿时间增加,因为需要扫描的内存太多。使用-Xmx..m,如果你的程序分配内存比较频繁,但是使用周期比较短,新生代必须要比较大的空间,可以使用-XX:NewSize=...
和-XX:MaxNewSize=...
或者1.4版本以后可以使用-Xmn
参数来调整新生代的大小。对于有些应用新生代比较大会有所帮助,但是也会引起minor gc时间长。对于大多数的程序来说,新生代的gc比其他代内存的回收要更快,因为大多数的对象在新生代声明周期就结束了。
如果你像这样设置参数:
-Xms384m -Xmx384m -XX:NewSize=128m -XX:MaxNewSize=128m
这指定将1/3的内存分配给了新生代,在1.3版本的jdk中,MaxNewSize参数在Sparc平台上设置为32mb, 在Intel基础的平台上为2.5mb。在Sparc服务器上NewRatio参数设置为2,在Intel机器上client模式设置为12, 其他平台上默认为8。可以看出来,MaxNewSize的默认值会覆盖这些设置。在1.4版本以后,MaxNewSize默认设置为无限大,NewRatio就可以用来设置新生代的大小。所以在1.4版本以后可以使用下面的设置方式来代替上面的设置:
-Xms384m -Xmx384m -XX:NewRatio=``2
如果比起停顿时间你更担心gc的次数,那么增加堆内存大小会减少full gc的次数,同样的你也可以增加新生代的大小来减少minor gc的次数
很多系统种的内存管理能力比HotSpot弱的多,在这种情况下,有些程序使用对象池的方式管理对象,但是,在HotSpot虚拟机中不要使用对象池,使用对象池会影响垃圾回收器对对象的及时释放,这会增加多余的垃圾。在现代的JVM虚拟机中,应该尽量少使用对象池的方式。
2. 我应该怎么分析堆内存使用?
你可以使用-Xaprof
参数来分析程序使用的内存的分配。
你也可以使用-agentlib:hprof=heap=all
来查看堆内存的使用(其他关于hprof的参数可以使用-agentlib:hprof=help
来查看参数列表)
从jdk 1.5以后,你也可以使用jmap来跟踪查看内存的使用情况
3. JVM出现"OutOfMemoryError”,但是增加堆大小并没有多大帮助,应该怎么处理?
JVM虚拟机在内存被完全使用并且没有swap空间可以使用的情况下是不能继续增长的,比如在多个内存消耗程序在并行运行的时候。当这种情况下,JVM会打印一条消息并退出:
Exception java.lang.OutOfMemoryError: requested <size> bytes
4. 32位JVM怎么使用更大的堆内存
理论上32位JVM能够使用的最大堆内存是4G。但是由于各种其他的限制,比如说交换空间,内核地址空间,内存随便,JVM管理等,实际上能使用的堆内存空间要小得多。在现在多少32位的windows系统里最大可用堆内存从1.4G到1.6G之间。在32位的Solaris内核的系统中,可用地址空间被限制在2G,在64位的操作系统上运行的32位JVM,最大堆内存可以更大一点,在多数Solaris系统中,可以接近4G。
从Java SE 6版本之后,Windows的boot.ini中的/3GB 开关不在生效。
如果你的程序需要非常大的内存,你应该使用64位操作系统上使用64位的JVM。
5. 我应该池化管理对象来协助GC吗?我应该间歇性调用System.gc()来释放内存吗?
答案是否定的!
池化对象会引起他们的生命周期比正常情况要长。这种情况使用垃圾回收器来做内存管理会更有效。我们强烈建议不要使用对象池。
不要调用System.gc()
。HotSpot可以决定什么时候来做垃圾回收,而且做的更好。如果你在垃圾回收停顿时间上有问题,请看上面gc停顿时间的问题。
6. 怎么判断软引用的对象什么时候应该被清理?
从1.3.1版本开始,软引用的对象从上次访问以后可以保留一段时间。保存的默认时间是堆剩余内存每多1m就多1s。这个值可以通过参数-XX:SoftRefLRUPolicyMSPerMB
来调整,这个参数是一个int类型的值,单位是毫秒,例如,把这个值从1s改成2.5s,我们可以使用:
-XX:SoftRefLRUPolicyMSPerMB=2500
Server JVM使用最大可用堆内存大小来计算剩余空间,可以通过-Xmx
设置。
Client JVM使用当前堆内存大小来计算剩余空间。
这个的意思就是,通常情况下,Server虚拟机更倾向于扩大堆内存而不是清理软引用,这个时候-Xmx
参数在清理软引用的时候是起作用的。
另外一个方面,Client虚拟机更倾向于清理软引用而不是扩大堆内存大小。
上面描述的行为对于1.3.1版本到Java SE 6版本的各个HotSpot虚拟机是有效的。这个行为不是JVM规范的行为。后面的版本可能会发生改变。像-XX:SoftRefLRUPolicyMSPerMB
这种参数不保证后面的版本还会存在。
1.3.1版本之前,Java HotSpot 虚拟机随时都会清理软引用。
7. 我打开-verbose:gc,发现有非常多的full gc发生,我对堆内存进行了优化但是并没有作用,该怎么办?
如果你在使用RMI,那么可能正在进行分布式垃圾回收。也有可能有一些应用显示的调用如System.gc()
想让他们的程序更快一些,幸运的是,在1.3版本和以后的版本中,你可以使用-XX:+DisableExplicitGC
参数禁用这种GC,使用-verbose:gc
看看修改是否有效
JIT编译问题
1. -client跟-server有什么区别
这两个系统是不同的二进制文件。他们本质是两个不同的JIT编译器。client系统最好是用在需要快速启动,资源占用比较少的程序。server系统最好用在需要整体性能要求比较高的系统。通常来将client系统更适合于交互性的程序比如说GUI程序。一些其他的区别还有比如说编译策略的却别,默认堆大小的区别和编译内联策略的区别等。
2. 我应该从哪里得到client跟server系统呢
client跟server系统在32位的Solaris跟Linux版本中是在一起的。对于32位的Windows系统,如果你只下载了JRE,里面只有client系统,如果你安装了JDK,里面会同时有client系统跟server系统
对于64位的版本,里面只有server系统。在Solaris系统里,64位的JRE是在32位的版本基础之上的。但是在Linux跟Windows里,他们是不同的版本
3. 我希望java默认运行在-server模式。我是用一堆脚本来改变它但是并没有作用。有什么方法改变吗?
自从Java SE 5.0以后,除了32位的Windows以外,在server-class的机器上(对于server-class的定义可以参考之前的文章)默认选择使用server虚拟机。server-class的定义可能各个版本有区别,所有可以查看版本对应的文档来确定。
4. 我应该在程序运行时先让代码"热热身"来保证JIT编译吗?
对于HotSpot来说是不必要的。HotSpot使用栈上分配技术可以动态替换正在循环解释执行的程序代码。不需要浪费你的程序时间来“热身”来获得更好的程序性能。
64位Java问题
1. 什么是64位Java
64位版本的Java在Solaris SPARC平台上可以从J2SE 1.4.0及之后的版本获得,64位的J2SE是Java SDK的一个实现,运行在64位的环境中,64位的操作系统,64位的CPU。你可以认为这个环境只是我们实现了SDK的另外一个平台。使用64位环境的主要好处就是更大的地址空间。这就允许使用更大的Java堆内存空间和更多的线程。这在一些长期运行或者大程序中是非常必要的。
做这样一个实现主要的问题是一些本地数据类型改变了。不意外的,指针的大小增大到了64位。在Solaris和大多数Unix平台上,C语言的long类型也增大到了64位。一些使用32位SDK实现的本地代码依赖于老的数据大小的代码现在需要做更新了。在这部分里,使用SDK写得Java程序就简单了,因为Java 严格限制他的原始类型大小。但是也有一些Java代码需要更新,比如说当一个Java的int值需要传给一个用C实现的部分时。
很多Java用户和开发者认为64位的实现就是jdk内置的java类型大小都从32变成64位了。这是不对的。我们没有把integer类型的大小从32位扩大到64位,而且Java的long类型本来就是64位的,他们不需要更新。数组的索引页遵守Java虚拟机规范,没有从32位扩展到64位。我们在实现第一个64位java实现时特别小心保证java的二进制文件跟API的兼容性,来保证100%的纯Java应用程序可以跟在32位虚拟机里一样的继续在64位虚拟机里运行
2. -client跟-server在64位Java中都是可用的吗?
目前只有Server虚拟机支持64位操作,-d64参数同时隐含-server的含义。在未来的版本中这个可能会改变。
3. 有哪些组件不支持64位的操作?
Java Plug-in, AWT Robot和Java Web Start现在不支持64位操作。如果需要这些特性需要使用32位的Java
4. 当写Java代码时,怎么区分32位跟64位操作?
32位和64位没有不同的公共API。你可以认为64位只是另外一个一次编写,处处运行的平台。然而,如果你喜欢写平台相关的代码,有一个系统参数sun.arch.data.model
可以使用来区分,值为32,64或者unknown
5. 32位JVM跟64位JVM的性能规格有什么差别?
通常来讲,大内存地址空间的使用相对于运行在32位的程序会造成一部分的性能损失。这是因为系统中每一个本地指针都占用8个字节而不是4个。这部分的额外的数据加载的影响取决于你的Java程序中使用了多少指针。好消息是在AMD64跟EM64T的64位模式下,Java虚拟机可以使用一些其他的寄存器用来生成更有效的本地指令序列。这些额外的寄存器补齐了由于指针引起的性能损失,所以在对比32位跟64位的执行速度时,经常发现基本没有性能损失。
所以,在SPARC平台下64位的JVM性能要比32位的性能低10-20%。在AMD64平台跟EM64T平台上这个差距大概在0-15%,具体取决于你程序使用的指针数量。
6. 在一个非常大的64位堆上我应该什么垃圾回收器?
64位的Java实现最大的优点就是可以创建和使用更多的Java对象。他可以突破2GB的内存限制。但是,在你程序的生命周期里必须要有更多的垃圾回收。如果你不把这个考虑进去,这些额外的垃圾回收会造成非常多的等待。HotSpotVM有很多针对大内存的垃圾回收器。我们建议在使用大内存的堆时启用并行或者并发的垃圾回收器。这些垃圾回收器使用与应用线程并发收集垃圾或者使用多CPU同时执行垃圾回收的方式来加速垃圾回收。关于垃圾回收的Hotspot GC调优可以参考指导:Tuning Garbage Collection with the 5.0 Java Virtual Machine.
多线程应用问题
1. 我有一个程序使用了很多线程而且运行很慢
对于Solaris很多线程的程序调优,可以参考Java and Solaris Threads Document
有一个没有文档化的参数,-Xconcurrentio
, 通常会对很多线程的应用有帮助,尤其是在Solaris系统中。使用-Xconcurrentio
参数最大的特点是使用轻量级进程LWP同步来代替线程的同步。我们发现对于一些程序速度可以提升超过40%。从1.4版本以后,LWP同步是默认值,但是-Xconcurrentio
仍然有帮助,因为他还开启一些其他内部的选项。最后,有一个可替代的线程库lwp在Solaris 9是默认的,在Solaris 8也可以使用,只需要改变LD_LIBRARY_PATH,将/usr/lib/lwp放到/usr/lib之前
2. 我的应用有很多线程,运行出现OutofMemory了,为什么?
这可能是线程默认栈大小的问题,在Java SE 6中,在Sparc平台32位jvm默认是512k,64位jvm默认是1024k,在x86会有的Solaris/Linux上,32位JVM默认320k,64位jvm默认1024k。
在Windows系统中,默认线程栈大小是从java.exe中读取的,对于Java SE 6来说,32位jvm默认是320k,64位jvm默认是1024k。
你可以使用-Xss参数来降低栈大小。例如:
java -server -Xss64k
注意在一些版本的windows中,操作系统会在非常粗的粒度上对线程栈大小进行取整。如果设置的大小比默认大小小1K以上,栈大小被取整到默认大小,否则栈大小被取整到1MB的倍数。
每个线程的栈空间至少需要64k
性能问题
1. 我的程序运行非常慢,为什么?
首先,要明白程序花在运行字节码的时间百分比,如果程序是I/O相关或者在执行本地方法,这种情况虚拟机没有在占用CPU时间。虚拟机技术只会提升运行字节码的速度。一个虚拟机没有在执行字节码的典型的例子是图形操作程序,大量的使用本地方法和IO操作比如读取写入网络数据和数据库文件。
假如虚拟大部分是在执行字节码,那么确保虚拟机在正确的模式下运行。如果程序需要快速启动占用空间较小,使用client模式,如果整体性能比较重要,使用server模式。
如果上面说的没有解决掉性能问题,可以参照前几篇文章中的参数说明,选择参数进行调优。还有一些可用工具如jstat, hprof等可以帮助解决应用性能问题。
2. 我的程序不能在多个CPU运行上运行?
CPU缩放问题是一个常见问题。首先,你的应用程序可能没有使用可伸缩的方式编写(比如如果你是用了大量的同步或者只有一个线程)。也有可能你是用了不可伸缩的操作系统资源,最后,如果你有很多线程,可能是垃圾回收正在运行。
关于GC伸缩的问题,可以看上面关于GC的问题和GC调优指南。
有可能是虚拟机是用线程模型有问题,关于这个的更多信息,可以看Java and Solaris Threads Document
3. 我的server应用不能运行更快,为什么?是I/O问题吗?
如果你阻塞在I/O操作,那么不管你使用的是什么版本的java程序都不能提升这个速度。如果你的应用使用了很多线程你可能遇到了可伸缩性的问题,关于诊断和解决线程伸缩性的问题,可以看文档Java and Solaris Threads Document
4. 我的应用程序使用了数据库,看起来好像可伸缩性不好,我应该怎么做?
Oracle提供了两种类型的数据库驱动,一个type-2驱动,叫OCI(Oracle Call Interface)驱动,使用本地代码实现,一个type-4纯Java驱动叫thin driver。在单核环境下,thin driver比OCI驱动工作的更好,因为OCI驱动的JNI管理等因素。在多核配置下,Solaris下使用OCI驱动的同步点编程阻碍可伸缩性的重要瓶颈。在Solaris系统中一个解决这种同步问题的方法就是使用libumen库。否则,我们建议使用thin driver。