jvm调优总结

peterjky 12年前

探究JVM内存泄露 性能调优的基本知识和JDK调优

基本知识

1.1 性能是什么

在性能调优之前,我们首先来了解一下性能是什么?关于性能,我想每个学习过 Java 的人都能列出几点,甚至可以夸夸其谈。在《 Java TM Platform Performance 》一书中,定义了如下五个方面来作为评判性能的标准:

1) 运算的性能——哪一个算法的执行性能最好?

2) 内存的分配——程序运行时需要耗费多少内存?

3) 启动的时间——程序启动需要多长时间?这在 Web 项目中的影响不大,但要注意部分程序需要部署或运行在客户端时的情形(比如 applet 程序)。

4) 程序的可伸缩性——在压力负载的情况下,程序的性能如何?

5) 性能的感知——用户在什么情况下会觉得程序的性能不好?

以上五个方面,在具体的使用场景可以有选择的去评判。至于这五方面的性能调优,在后续的章节中将会陆续的给以相应的性能调优策略。

1.2 调优的规则

我们只需要关心对我们程序有影响,可以察觉到的性能问题,而不是每一个类中的每一个方法我们都需要想方设法的提高性能。如果程序的性能没有达到我们所期望的要求,我们才需要考虑如何优化性能。同样的,晦涩的代码虽然提高了程序的性能,但同时可能带给我们的是维护的噩梦。我们需要折中的考虑以上两种情况,使得程序的代码是优美的,并且运行的足够快,达到客户所期望的性能要求。

优化代码甚至会导致不良的结果, Donald Knuth (一位比较牛比较有影响的人物,具体是谁,我也忘了,谁知道,可以告诉我一下,谢谢!)曾说过,“ Premature optimization is the root of all evil” 。在开始性能调优前,需要先指出不优化代码的一些理由。

1) 如果优化的代码已经正常工作,优化后可能会引入新的 bug

2) 优化代码趋向于使代码更难理解和维护;

3) 在一个平台上优化的代码,在另一个平台上可能更糟;

4) 花费很多时间在代码的优化上,提高了很少的性能,却导致了晦涩的代码。

确实,在优化前,我们必须认真的考虑是否值得去优化。

1.3 调优的步骤

一般我们提高应用程序的性能划分为以下几个步骤:

1) 明确应用程序的性能指标,怎样才符合期望的性能需求;

2) 在目标平台进行测试;

3) 如果性能已经达到性能指标, Stop

4) 查找性能瓶颈;

5) 修改性能瓶颈;

6) 返回到第 2 步。

JDK 调优

2.1 选择合适的 JDK 版本

不同版本的 JDK ,甚至不同厂家的 JDK 可能都存在着很大的差异,对于性能优化的程度不同。一般来说,尽可能选择最新发布的稳定的 JDK 版本。最新的稳定的 JDK 版本相对以前的 JDK 版本都会做一些 bug 的修改和性能的优化工作。

2.2 垃圾收集 Java 堆的优化

垃圾收集就是自动释放不再被程序所使用的对象的过程。当一个对象不再被程序所引用时,它所引用的堆空间可以被回收,以便被后续的新对象所使用。垃圾收集器必须能够断定哪些对象是不再被引用的,并且能够把它们所占据的堆空间释放出来。如果对象不再被使用,但还有被程序所引用,这时是不能被垃圾收集器所回收的,此时就是所谓的“内存泄漏”。监控应用程序是否发生了内存泄漏,有一个非常优秀的监控工具推荐给大家——Quest 公司的 JProbe 工具,使用它来观察程序运行期的内存变化,并可产生内存快照,从而分析并定位内存泄漏的确切位置,可以精确定位到源码内。这个工具的使用我在后续的章节中还会做具体介绍。

Java 堆是指在程序运行时分配给对象生存的空间。通过 -mx/-Xmx -ms/-Xms 来设置起始堆的大小和最大堆的大小。根据自己 JDK 的版本和厂家决定使用 -mx -ms -Xmx -Xms Java 堆大小决定了垃圾回收的频度和速度, Java 堆越大,垃圾回收的频度越低,速度越慢。同理, Java 堆越小,垃圾回收的频度越高,速度越快。要想设置比较理想的参数,还是需要了解一些基础知识的。

Java 堆的最大值不能太大,这样会造成系统内存被频繁的交换和分页。所以最大内存必须低于物理内存减去其他应用程序和进程需要的内存。而且堆设置的太大,造成垃圾回收的时间过长,这样将得不偿失,极大的影响程序的性能。以下是一些经常使用的参数设置:

1) 设置 -Xms 等于 -XmX 的值;

2) 估计内存中存活对象所占的空间的大小,设置 -Xms 等于此值, -Xmx 四倍于此值;

3) 设置 -Xms 等于 -Xmx 1/2 大小;

4) 设置 -Xms 介于 -Xmx 1/10 1/4 之间;

5) 使用默认的设置。

大家需要根据自己的运行程序的具体使用场景,来确定最适合自己的参数设置。

除了 -Xms -Xmx 两个最重要的参数外,还有很多可能会用到的参数,这些参数通常强烈的依赖于垃圾收集的算法,所以可能因为 JDK 的版本和厂家而有所不同。但这些参数一般在 Web 开发中用的比较少,我就不做详细介绍了。在实际的应用中注意设置 -Xms -Xmx 使其尽可能的优化应用程序就行了。对于性能要求很高的程序,就需要自己再多研究研究 Java 虚拟机和垃圾收集算法的机制了。可以看看曹晓钢翻译的《深入 Java 虚拟机》一书。


WEB 服务总是莫名其妙的运行一段时间后 JVM 直接 OutOfMemory 错误,内存泄漏的问题不容易查找,本文就一些查找内存泄露基本知识做个总结,未涉及到具体案例的分析。

1 JVM 内存异常的数据显示
1.1 java.lang.OutOfMemoryError: PermGen space 异常的例子

Heap
PSYoungGen total 44928K, used 916K [0x4e3c0000, 0x50fe0000, 0x51b10000)
eden space 44736K, 2% used [0x4e3c0000,0x4e4a5318,0x50f70000)
from space 192K, 0% used [0x50f70000,0x50f70000,0x50fa0000)
to space 192K, 0% used [0x50fb0000,0x50fb0000,0x50fe0000)
PSOldGen total 453312K, used 125529K [0x32910000, 0x4e3c0000, 0x4e3c0000)
object space 453312K, 27% used [0x32910000,0x3a3a6498,0x4e3c0000)
PSPermGen total 65536K, used 65535K [0x2e910000, 0x32910000, 0x32910000)
object space 65536K, 99% used [0x2e910000,0x3290fff8,0x32910000)

permanent space 持久空间 : 用于类和方法对象的存储。 spring AOP 时使用 CBLIB 会动态产生很多类,当类太多,超过 MaxPermSize 的时候,就会抛出此异常。 参数问题 可以设置 jvm 启动参数 : PermSize, MaxPermSize 。程序问题就要进行内存分析了,详见下文。

1.2 java.lang.OutOfMemoryError: Java heap space 异常的例子

Heap
PSYoungGen total 88320K, used 67673K [0x44880000, 0x4ba40000, 0x4ba40000)
eden space 61952K, 100% used [0x44880000,0x48500000,0x48500000)
from space 26368K, 21% used [0x48500000,0x48a96490,0x49ec0000)
to space 24512K, 16% used [0x4a250000,0x4a6283e0,0x4ba40000)
PSOldGen total 932096K, used 582090K [0x0ba40000, 0x44880000, 0x44880000)
object space 932096K, 62% used [0x0ba40000,0x2f2b2a78,0x44880000)
PSPermGen total 131072K, used 35124K [0x03a40000, 0x0ba40000, 0x0ba40000)
object space 131072K, 26% used [0x03a40000,0x05c8d330,0x0ba40000)

eden space 使用率 100% ,总是被占满,参数问题 可以设置 jvm 启动参数 : Xms, Xmx 。程序问题就要进行内存分析了,详见下文。

1.3 查看 jvm 内存状态:

jstat -gcutil pid 1000 20

异常情况的例子

jstat -gcutil pid 1000 20

S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 99.99 82.51 53.11 2409 1.205 10117 7250.393 7251.598
0.00 0.00 83.42 82.55 53.10 2409 1.205 10118 7252.650 7253.855
0.00 0.00 56.06 82.46 53.10 2410 1.205 10120 7254.467 7255.672
0.00 0.00 32.11 82.55 53.10 2411 1.205 10121 7256.673 7257.877
0.00 0.00 99.99 82.55 53.10 2412 1.205 10123 7257.026 7258.231
0.00 0.00 76.00 82.50 53.10 2412 1.205 10124 7259.241 7260.446

这个数据显示 Full GC 频繁发生。

正常情况的例子

S0 S1 E O P YGC YGCT FGC FGCT GCT

0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031

0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031

0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031

0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031

0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031

0.00 0.00 0.24 55.39 99.60 171 0.667 1339 393.364 394.031

参数含义:
S0
Heap 上的 Survivor space 0 段已使用空间的百分比
S1
Heap 上的 Survivor space 1 段已使用空间的百分比
E
Heap 上的 Eden space 段已使用空间的百分比
O
Heap 上的 Old space 段已使用空间的百分比
P
Perm space 已使用空间的百分比
YGC
:从程序启动到采样时发生 Young GC 的次数
YGCT
Young GC 所用的时间 ( 单位秒 )
FGC
:从程序启动到采样时发生 Full GC 的次数
FGCT
Full GC 所用的时间 ( 单位秒 )
GCT
:用于垃圾回收的总时间 ( 单位秒 )

2 Dump 出内存
2.1 找出要 dump 的线程 pid

windows 下,使用 tasklist

Linux 下,使用 ps –aux

2.2 Dump 出内存使用详情

可以通过命令:

jmap -dump:file=a.hprof pid

也可以通过 jconsole 的图形界面操作。

在命令行键入: jconsole

Jconsole 打开后在造作下选择 dumpHeap, 输入参数 p0,p1;p0 表示 dump 出来的文件路径,后缀为 .hprof p1 设为 true ,表示只分析活着的对象。

jvm调优总结

3 使用内存分析工具

目前有很多用来分析 Java 内存对象的工具,如收费的工具有 jprofiler, 而像 Eclipse MAT 则是优秀的内存对象分析开源工具 . 它们对于分析内存溢出问题非常有用。以下是一个安装使用 Eclipse MAT 的简单例子。

3.1 装一个 Eclipse 的内存分析插件 MAT

http://download.eclipse.org/technology/mat/latest/update-site/

3.2 切换到 Memory Analysis 模式
jvm调优总结

3.3 通过 File > Open Heap Dump.... 查看 dump 出来的文件

jvm调优总结

4 JDK 自带的 JVM 查看分析工具 jps jmap jstat jconsole
4.1 jps

Java 进程查看工具,实际上它和 Unix/Linux 上面的 ps 命令的功能差不多

4.2 jmap

jmap 是一个可以输出所有内存中对象的工具 .

* -dump:format=b,file=<filename> 转存堆内存到本地文件。
* -histo
打印堆里每个类的情况,包含内存占用大小、对象数量及完整类名。 VM 的内部类以 "*" 开头。

例子:

jmap -histo pid>a.log

jmap -dump: file=a.hprof pid

查看 a.log

num #instances #bytes class name
--------------------------------------
1: 427398 14458448 [I
2: 178798 6830216 [C
3: 50278 6668512 <constMethodKlass>
4: 179924 4318176 java.lang.String
5: 50278 4026648 <methodKlass>
6: 15244 3894200 [B
7: 47809 1773776 [Ljava.lang.Object;
...
Total 1645187 81806088

说明:

#instance 是对象的实例个数
#bytes
是总占用的字节数
class name
对应的就是 Class 文件里的 class 的标识
B
代表 byte
C
代表
char
D
代表
double
F
代表
float
I
代表
int
J
代表
long
Z
代表
boolean
前边有 [ 代表数组, [I 就相当于 int[]

对象用 [L+ 类名表示

4.3 jstat

jstat vm 的状态监控工具,监控的内容有类加载、运行时编译及 GC

使用时,需加上查看进程的进程 id ,和所选参数。以下详细介绍各个参数的意义。
jstat -class pid:
显示加载 class 的数量,及所占空间等信息。
jstat -compiler pid:
显示 VM 实时编译的数量等信息。
jstat -gc pid:
可以显示 gc 的信息,查看 gc 的次数,及时间。其中最后五项,分别是 young gc 的次数,young gc 的时间, full gc 的次数, full gc 的时间, gc 的总时间。
jstat -gccapacity:
可以显示, VM 内存中三代( young,old,perm )对象的使用和占用大小,如: PGCMN显示的是最小 perm 的内存使用量, PGCMX 显示的是 perm 的内存最大使用量, PGC 是当前新生成的 perm 内存占用量, PC 是但前 perm 内存占用量。其他的可以根据这个类推, OC old 内纯的占用量。
jstat -gcnew pid:new
对象的信息。
jstat -gcnewcapacity pid:new
对象的信息及其占用量。
jstat -gcold pid:old
对象的信息。
jstat -gcoldcapacity pid:old
对象的信息及其占用量。
jstat -gcpermcapacity pid: perm
对象的信息及其占用量。
jstat -util pid:
统计 gc 信息统计。
jstat -printcompilation pid:
当前 VM 执行的信息。
除了以上一个参数外,还可以同时加上 两个数字,如: jstat -printcompilation 3024 250 6 是每 250毫秒打印一次,一共打印 6 次,还可以加上 -h3 每三行显示一下标题。
例子:

jstat -gcutil pid 1000 20

4.4 jconsole

一个 java GUI 监视工具,可以以图表化的形式显示各种数据。并可通过远程连接监视远程的服务器 VM