目录

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
2
*    soft   nofile   65535
*    hard   nofile   65535

修改保存以后,注销当前用户重新登录就生效了。

内存与垃圾回收问题

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。