垃圾收集器很大程度上会影响应用的吞吐量和延迟。在调优之前,首先要监控,继而进行分析。那我们先看一下如何获取GC数据,并理解这些数据。
获取GC数据
可以在命令行中指定下面一种选项:
- -verbose:gc或-verbosegc
- -XX:+PrintGCDetails——获取的信息比-verbose:gc更多
如果要获取GC执行的时间,计算GC的持续时间和频率,可以在命令行中指定任一选项:
- -XX:+PrintGCTimeStamps——显示从JVM启动到执行GC时流逝的时间,单位是秒
- -XX:+PrintGCDateStamps——显示执行GC时的本地时间(Java 6 update 4才开始支持)
要想观察GC的开销,或者看应用的延迟是应用本身的延迟,还是JVM GC的stop-the-world阶段的开销,可以指定-XX:+PrintGCApplicationConcurrentTime和-XX:+PrintGCApplicationStoppedTime选项。
另外还可以使用-Xloggc:<fileName>选项将GC数据直接输出到文件中,以便进行离线分析。
输出示例
常规输出
下面是Java 7 update 71(不同版本的输出可能会略有不同)上使用-XX:+PrintGCDetails获取的输出示例:
22.435: [Full GC [PSYoungGen: 638K->0K(36352K)] [ParOldGen: 8507K->9082K(44032K)] 9146K->9082K(80384K) [PSPermGen: 14392K->14392K(21504K)], 0.0547773 secs] [Times: user=0.08 sys=0.00, real=0.06 secs]
第一条信息:
GC说明是minor GC(年轻代GC)。
PSYoungGen表示收集使用的收集器是多线程垃圾收集器Parallel Scavenge。
1151K->638K(36352K)中,1151K表示收集前年轻代占用的内存,638K表示收集后年轻代占用的内存。由于年轻代分为Eden和两个Suvivor,minor收集后Eden为空,所以该值也是Survivor的占用量。括号中的36352K是年轻代的大小。
9659K->9146K(80384K)中,9659K表示收集前整个堆占用的内存,9146K表示收集后整个堆占用的内存。括号里的80384K是Java堆的总量,可以算出老年代的大小是44032K(80384K-36352K)。
0.0032716是整次GC的暂停时间。
[Times: user=0.03 sys=0.00, real=0.00 secs]里,user是垃圾收集器执行非操作系统调用指令所耗费的CPU时间,sys是垃圾收集器执行操作系统调用所耗费的CPU时间, real表示完成GC的实际时间。
GC之后,年轻代减少了513K(1151K-638K),老年代占用的空间则没有变化((9146K-638K)-(9659K-1151K)),这说明年轻代里的对象都被回收了,没有晋升到老年代中。
第二条信息:
Full GC说明是full GC,是对整个堆进行GC,包括年轻代、老年代和永久代。
PSYoungGen: 638K->0K(36352K)的含义和第一条信息里的PSYoungGen: 1151K->638K(36352K)一样。
ParOldGen: 8507K->9082K(44032K)表示老年代收集使用的是多线程垃圾收集器Parallel Old,收集前老年代占用8507K,收集后占用9082K,老年代的大小是44032K。
PSPermGen: 14392K->14392K(21504K)说明GC前永久代的占用量是14392K,GC后永久代的占用量是14392K,而永久代的大小是21504K。
带延迟信息的输出
下面是Java 7 update 71上使用-XX:+PrintGCDetails和-XX:+PrintGCApplicationConcurrentTime、-XX:+PrintGCApplicationStoppedTime选项获取的输出示例:
21.719: [GC [PSYoungGen: 1815K->64K(15360K)] 10319K->8567K(49664K), 0.0035923 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
21.723: [Full GC [PSYoungGen: 64K->0K(15360K)] [ParOldGen: 8503K->8523K(34304K)] 8567K->8523K(49664K) [PSPermGen: 14392K->14392K(22016K)], 0.0500793 secs] [Times: user=0.12 sys=0.00, real=0.05 secs]
21.773: Total time for which application threads were stopped: 0.0542506 seconds
在这个片段中,应用运行了1.83秒,GC用了0.054秒,此次GC大约有3%的开销。
显式GC
如果GC输出信息中包含(System),那说明这次GC是应用显式调用System.gc()所引起的。Java通常不建议应用进行显式的垃圾收集,要是在GC输出中发现该信息,就需要调查代码显式GC的原因了。
[Full GC (System) [PSYoungGen: ......
小结
通常来说,如果minor GC时,年轻代里的对象大部分被晋升到了老年代,被GC的并不多,那可以说明年轻代太小了。但在minor GC前后,老年代的大小要是没有发生变化,那意味着没有对象从年轻代提升到老年代。这是个好现象,说明minor GC回收了较多的垃圾对象,可以减少full GC的频率(full GC的持续时间很长)。
如果日志里只有full GC,每次full GC后,老年代几乎没回收什么空间,而且年轻代总有很大的占用量,那很可能是老年代空间不够大。
GC调优就基于对数据的分析,以及应用追求的性能指标(吞吐量、延迟、抑或内存占用),然后调整相应的设置,再进行新一轮的“监控——分析——调优”,……,直到满足应用的性能指标。