因为线上系统遇到CPU100%的问题,这种问题在流量较大时比较常见,因为JDK自身有很多JVM调试工具,如jps、jstack、jmap、jhat、jstat等使用工具,在实际工作中使用这些工具进行调试是十分必要的,一般通过以下步骤就能定位并解决CPU100%的问题,文章是自己很早写的,现在重拾一下,下面只做简单介绍,这些都可以在线上服务器上执行。
一、 jstack
jstack用来查Java进程内的线程堆栈信息。语法格式如下:
-l,除了堆栈打印出额外的锁信息,在发生死锁时可以用jstack -l pid来观察锁持有情况 |
-m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native方法) |
在Deploy里面的jstack导出的堆栈信息如下,我们在自动部署里面只使用-l命令,可以看到进程为30086,在堡垒机里面同样是此进程。
图1 自动部署平台jstack -l堆栈信息
图2 堡垒机查看应用进程id
由于jstack导出的栈信息比较多,通过这些信息可以定位到当前所有线程的运行状况、锁状态等等。
另外如果线上发生cpu使用过高的情况,jstack则可以快速定位到线程堆栈,具体的操作思路是获java进程中最耗费cup的线程,并得出其堆栈信息,根据堆栈定位到具体代码,这在JVM性能调试中使用得很多,这个操作得再堡垒机上运行.
第一步先找出应用的进程ID,这个在上文已经说过,本文使用的是是30086:
接下来得到当前进程内最耗费CPU的线程,使用top -Hp 30086,输出如下:
图3 查看最耗时的线程
TIME列就是各个Java线程耗费的CPU时间,CPU时间最长的是线程ID为30531的线程,用
得到30531的十六进制值为7743,之后使用jstack它用来输出进程30531的堆栈信息,然后根据线程ID的十六进制值grep,如下:
因为在系统稳定运行期间平时并没有死锁和等待锁,此处发现7743是ump线程,并没有异常。
"UMP-WriteLog2FileThread-aliveLogger" daemon prio=10 tid=0x00007f8a04010000 nid=0x7743 sleeping[0x00007f8ac2ae9000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.jd.ump.profiler.util.MessagesStore$WriteLog2File.run(MessagesStore.java:85)
at java.lang.Thread.run(Thread.java:745)
Locked ownable synchronizers:
- None
而30089线程对应的0x7589则为gc线程。
"GC task thread#0 (ParallelGC)" prio=10 tid=0x00007f8c60020000 nid=0x7589 runnable
而在cpu暴增的情况下,观察jstack线程,会得到以下的新增,定位到具体代码。
jmap用来查看堆内存使用状况,这个可以直接在deploy里面使用,最后在日志路径下载
jmap语法格式如下:
查看进程的类加载器和类加载器加载的永久代对象信息,输出:类加载器名称、对象是否存活(不可靠)、对象地址、父类加载器、已加载的类大小等信息,如下图:
图5 类加载器信息
jmap -heap pid查看进程堆内存使用情况,包括使用的GC算法、堆配置参数和各代中堆内存使用情况,因为已经调整过堆内存为2G,而NewRatio的比例为2,说明新生代和老年代的大小为1比2,老年代过大很多时候使得内存无法释放,同时老年代过大,很大程度上浪费了内存,这里可以在在启动的环境变量中修改-XX:NewRatio=1
,可以找运维调整一下。
PROCESS PATH:/export/Domains/m.jd.id/server1
DATETIME: 2016-06-01 08:51:28
EXECUTE COMMAND:/export/servers/jdk1.7.0_71/bin/jmap -heap 30086
using thread-local object allocation.
Parallel GC with 23 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 1310720 (1.25MB)
MaxNewSize = 17592186044415 MB
OldSize = 5439488 (5.1875MB)
NewRatio = 2
SurvivorRatio = 8
PermSize = 21757952 (20.75MB)
MaxPermSize = 1073741824 (1024.0MB)
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 704643072 (672.0MB)
used = 355307136 (338.8472900390625MB)
free = 349335936 (333.1527099609375MB)
50.42370387486049% used
From Space:
capacity = 5767168 (5.5MB)
used = 3899552 (3.718902587890625MB)
free = 1867616 (1.781097412109375MB)
67.61641068892045% used
To Space:
capacity = 5767168 (5.5MB)
used = 0 (0.0MB)
free = 5767168 (5.5MB)
0.0% used
PS Old Generation
capacity = 1431830528 (1365.5MB)
used = 1389572200 (1325.199317932129MB)
free = 42258328 (40.300682067871094MB)
97.04865015980438% used
PS Perm Generation
capacity = 110100480 (105.0MB)
used = 58882144 (56.154388427734375MB)
free = 51218336 (48.845611572265625MB)
53.480369931175595% used
35733 interned Strings occupying 4872992 bytes.
还有一个应该常用的情况是:用jmap把进程内存使用情况dump到文件中,再用jhat分析查看。jmap进行dump命令格式如下:
jmap -dump: format =b, file =dumpFileName |
我们同样在deploy里面使用Dump:
图6自动部署dump文件命令
dump出来的文件用IBM heapAnalysis工具查看,使用命令打开下载下来的dump文件:
java -Xmx3000m -jar ha396.jar fileName
图7 IBM分析堆内存使用大写
图8 TreeView查看具体使用场合
分析代码,在压测时,cpu接近100%,而堆内存中有接近一半是HashMap,占了44%,800多M。
再使用TreeView我们发现,大部分的HashMap是tomcat在处理request时使用,这也与压测时每分钟上万的请求不谋而合。
语法格式如下:
1 |
jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ] |
vmid是虚拟机ID,在Linux/Unix系统上就是进程ID。interval是采样时间间隔。count是采样数目。比如下面输出的是GC信息,采样时间间隔为500ms,采样数为20,我们采集从8点48分53秒,国内时间是9点48分53秒:
PROCESS PATH:/export/Domains/m.jd.id/server1
DATETIME: 2016-06-01 08:48:53
EXECUTE COMMAND:/export/servers/jdk1.7.0_71/bin/jstat -gc 30086 500 20
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
9728.0 10240.0 0.0 864.0 678912.0 635616.5 1398272.0 1249792.7 107520.0 57499.5 2433 474.174 1 6.717 480.891
9728.0 9728.0 1824.0 0.0 679936.0 153569.9 1398272.0 1249976.7 107520.0 57499.5 2434 474.433 1 6.717 481.150
9728.0 9728.0 1824.0 0.0 679936.0 359170.8 1398272.0 1249976.7 107520.0 57499.5 2434 474.433 1 6.717 481.150
9728.0 9728.0 1824.0 0.0 679936.0 558627.4 1398272.0 1249976.7 107520.0 57499.5 2434 474.433 1 6.717 481.150
9728.0 9728.0 0.0 5168.6 679936.0 17135.0 1398272.0 1250152.7 107520.0 57499.5 2435 474.582 1 6.717 481.299
9728.0 9728.0 0.0 5168.6 679936.0 273388.6 1398272.0 1250152.7 107520.0 57499.5 2435 474.582 1 6.717 481.299
9728.0 9728.0 0.0 5168.6 679936.0 448408.6 1398272.0 1250152.7 107520.0 57499.5 2435 474.582 1 6.717 481.299
9728.0 9728.0 0.0 5168.6 679936.0 672819.4 1398272.0 1250152.7 107520.0 57499.5 2435 474.582 1 6.717 481.299
9728.0 12288.0 9697.6 0.0 674816.0 130122.3 1398272.0 1252427.5 107520.0 57499.5 2436 474.825 1 6.717 481.542
9728.0 12288.0 9697.6 0.0 674816.0 336255.0 1398272.0 1252427.5 107520.0 57499.5 2436 474.825 1 6.717 481.542
9728.0 12288.0 9697.6 0.0 674816.0 529050.8 1398272.0 1252427.5 107520.0 57499.5 2436 474.825 1 6.717 481.542
9728.0 12288.0 9697.6 5104.5 674816.0 674816.0 1398272.0 1259470.2 107520.0 57499.5 2437 474.825 1 6.717 481.542
11776.0 12288.0 0.0 5360.5 674816.0 210927.2 1398272.0 1259470.2 107520.0 57499.5 2437 475.020 1 6.717 481.737
11776.0 12288.0 0.0 5360.5 674816.0 423921.2 1398272.0 1259470.2 107520.0 57499.5 2437 475.020 1 6.717 481.737
11776.0 12288.0 0.0 5360.5 674816.0 630101.4 1398272.0 1259470.2 107520.0 57499.5 2437 475.020 1 6.717 481.737
11776.0 11264.0 5376.7 0.0 676352.0 143384.7 1398272.0 1262235.9 107520.0 57499.5 2438 475.073 1 6.717 481.790
11776.0 11264.0 5376.7 0.0 676352.0 336295.4 1398272.0 1262235.9 107520.0 57499.5 2438 475.073 1 6.717 481.790
11776.0 11264.0 5376.7 0.0 676352.0 488193.1 1398272.0 1262235.9 107520.0 57499.5 2438 475.073 1 6.717 481.790
10752.0 11264.0 0.0 4240.5 676352.0 26658.1 1398272.0 1266075.6 107520.0 57499.5 2439 475.195 1 6.717 481.912
10752.0 11264.0 0.0 4240.5 676352.0 239910.6 1398272.0 1266075.6 107520.0 57499.5 2439 475.195 1 6.717 481.912
要明白上面各列的意义,先看JVM堆内存布局:
可以看出:
2 |
年轻代 = Eden区 + 两个Survivor区(From和To) |
现在来解释各列含义:
S0C、S1C、S0U、S1U:Survivor 0/1区容量和使用量 |
FGC、FGCT:Full GC次数和Full GC耗时 |
从上面的数据可以看到,在该时间段内,10秒内供发生了5次youngGC,这也jvm监控里面的数据也是保持一致的,这是压测时候的表现,同时老年代内存使用率很高,然后并没有触发fullGC,我们可以适当的调整fullGC策略,新生代(Young generation)的空间太小,导致有一些本应该可以很快就被回收的对象在促销秒杀瞬间,频繁youngGc后被放到了老生代(Old generation)里,而老年代在之后一直得不到释放。此时使用上文提示的的-XX:NewRatio=1可以适当调整eden区大小。
图9 JVM监控GC数据
以上就是基于JVM调试工具的部分使用心得,更多命令和细节在网上也能找到,这里就不一一列举了。