【基础+实战】JVM原理及优化系列之九:JVM监控、分析与故障处理实战
1. 监控工具
-
jvisualvm(JDK内置)
-
jconsole(JDK内置)
-
jmc(JDK内置)
-
Jprofile(第三方)
-
Eclipse Memory Analyzer
-
JvisualVM插件
2. JAVA命令行工具
2.1 jps虚拟机进程状况工具
常用的几个参数:
-l 输出 java 应用程序的 main class 的完整包
-q 仅显示 pid ,不显示其它任何相关信息
-m 输出传递给 main 方法的参数
-v 输出传递给 JVM 的参数。在诊断 JVM 相关问题的时候,这个参数可以查看 JVM 相关参数的设置
2.2 jstat****虚拟机统计信息监视工具
jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ]
vmid 是虚拟机 ID ,在 Linux/Unix 系统上一般就是进程 ID 。 interval 是采样时间间隔。 count 是采样数目。比如下面输出的是 GC 信息,采样时间间隔为 250ms ,采样数为 4 :
1 2 3 4 5 6 | root@ubuntu:/# jstat -gc 21711 250 4 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 192.0 192.0 64.0 0.0 6144.0 1854.9 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 192.0 192.0 64.0 0.0 6144.0 1972.2 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 192.0 192.0 64.0 0.0 6144.0 1972.2 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 192.0 192.0 64.0 0.0 6144.0 2109.7 32000.0 4111.6 55296.0 25472.7 702 0.431 3 0.218 0.649 |
1
要明白上面各列的意义,先看 JVM 堆内存布局:
1 2 | 堆内存 = 年轻代 + 年老代 + 永久代 年轻代 = Eden区 + 两个Survivor区(From和To) |
1
** **现在来解释各列含义:
1 2 3 4 5 6 7 | S0C、S1C、S0U、S1U:Survivor 0/1区容量(Capacity)和使用量(Used) EC、EU:Eden区容量和使用量 OC、OU:年老代容量和使用量 PC、PU:永久代容量和使用量 YGC、YGCT:年轻代GC次数和GC耗时 FGC、FGCT:Full GC次数和Full GC耗时 GCT:GC总耗时 |
1
2.3 jinfo****配置信息工具
观察运行中的 java 程序的运行环境参数:参数包括 Java System 属性和 JVM 命令行参数
实例: jinfo 2083
其中 2083 就是 java 进程 id 号,可以用 jps 得到这个 id 号。
输出内容太多了,不在这里一一列举,大家可以自己尝试这个命令。
2.4 jmap****内存映像工具
jmap ( Memory Map )和 jhat ( Java Heap Analysis Tool )
jmap 用来查看堆内存使用状况,一般结合 jhat 使用。
jmap 语法格式如下:
1 2 3 | jmap [option] pid jmap [option] executable core jmap [option] [server-id@]remote-hostname-or-ip |
1
1 | jmap -permstat pid |
1
打印进程的类加载器和类加载器加载的持久代对象信息,输出:类加载器名称、对象是否存活(不可靠)、对象地址、父类加载器、已加载的类大小等信息
使用jmap -heap pid 查看进程堆内存使用情况,包括使用的 GC 算法、堆配置参数和各代中堆内存使用情况。
使用jmap -histo[:live] pid 查看堆内存中的对象数目、大小统计直方图,如果带上 live 则只统计活对象
还有一个很常用的情况是:用 jmap 把进程内存使用情况 dump 到文件中,再用 jhat 分析查看。 jmap 进行 dump 命令格式如下:
1 | jmap -dump:format=b,file=dumpFileName |
1
1 2 3 | root@ubuntu:/# jmap -dump:format=b,file=/tmp/dump.dat 21711 Dumping heap to /tmp/dump.dat ... Heap dump file created |
1
dump 出来的文件可以用MAT****、VisualVM 等工具查看,这里用jhat 查看
1 2 3 4 5 6 7 8 9 10 | root@ubuntu:/# jhat -port 9998 /tmp/dump.dat Reading from /tmp/dump.dat... Dump file created Tue Jan 28 17:46:14 CST 2014 Snapshot read, resolving... Resolving 132207 objects... Chasing references, expect 26 dots.......................... Eliminating duplicate references.......................... Snapshot resolved. Started HTTP server on port 9998 Server is ready. |
1
然后就可以在浏览器中输入主机地址 :9998 查看
2.5 jstack命令(Java Stack Trace)
jstack 主要用来查看某个 Java 进程内的线程堆栈信息。语法格式如下:
jstack [option] pid jstack [option] executable core jstack [option] [server-id@]remote-hostname-or-ip
命令行参数选项说明如下:
-l long listings,会打印出额外的锁信息,在发生死锁时可以用 jstack -l pid来观察锁持有情况 -m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native 方法) | -l long listings,会打印出额外的锁信息,在发生死锁时可以用 jstack -l pid来观察锁持有情况 -m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native 方法) |
-l long listings,会打印出额外的锁信息,在发生死锁时可以用 jstack -l pid来观察锁持有情况 -m mixed mode,不仅会输出Java堆栈信息,还会输出C/C++堆栈信息(比如Native 方法) |
1
3. 监控与分析
3.1 堆信息查看
3.1.1 用途
有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
-- 年老代年轻代大小划分是否合理
-- 内存泄漏
-- 垃圾回收算法设置是否合理
3.1.2 可查看内容
可查看堆空间大小分配(年轻代、年老代、持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)
查看堆内类、对象信息查看:数量、类型等
对象引用情况查看
3.2 线程监控
3.2.1 用途
Dump 线程详细信息:查看线程内部运行情况
死锁检查
线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下
3.3 热点分析(抽样器)
** CPU****热点** :检查系统哪些方法占用的大量 CPU 时间
** **内存热点 :检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)
这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
3.3.1 查看方法CPU耗时
3.3.2 查看线程CPU耗时
3.3.3 查看线程内存分配情况
3.3.4 查看对象占用内存情况
3.3.5 查看持久代内存占用情况
3.4 快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问题。
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。
3.5 缓冲区查看
3.5.1 可视化垃圾回收
4. JAVA基础命令详解
4.1 javac
用法:javac <选项> <源文件>
其中,可能的选项包括:
-g 生成所有调试信息
-g:none 不生成任何调试信息
-g:{lines,vars,source} 只生成某些调试信息
-nowarn 不生成任何警告
-verbose 输出有关编译器正在执行的操作的消息
-deprecation 输出使用已过时的 API 的源位置
-classpath <路径> 指定查找用户类文件的位置
-cp <路径> 指定查找用户类文件的位置
-sourcepath <路径> 指定查找输入源文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
-extdirs <目录> 覆盖安装的扩展目录的位置
-endorseddirs <目录> 覆盖签名的标准路径的位置
-d <目录> 指定存放生成的类文件的位置
-encoding <编码> 指定源文件使用的字符编码
-source <版本> 提供与指定版本的源兼容性
-target <版本> 生成特定 VM 版本的类文件
-version 版本信息
-help 输出标准选项的提要
-X 输出非标准选项的提要
-J<标志> 直接将 <标志> 传递给运行时系统
4.2 jar
用法:jar {ctxu}[vfm0Mi] [jar-文件] [manifest-文件] [-C 目录] 文件名 ...
选项:
-c 创建新的存档
-t 列出存档内容的列表
-x 展开存档中的命名的(或所有的〕文件
-u 更新已存在的存档
-v 生成详细输出到标准输出上
-f 指定存档文件名
-m 包含来自标明文件的标明信息
-0 只存储方式;未用ZIP压缩格式
-M 不产生所有项的清单(manifest〕文件
-i 为指定的jar文件产生索引信息
-C 改变到指定的目录,并且包含下列文件:
如果一个文件名是一个目录,它将被递归处理。
清单(manifest〕文件名和存档文件名都需要被指定,按'm' 和 'f'标志指定的相同顺序。
示例1:将两个class文件存档到一个名为 'classes.jar' 的存档文件中:
jar cvf classes.jar Foo.class Bar.class
示例2:用一个存在的清单(manifest)文件 'mymanifest' 将 foo/ 目录下的所有
文件存档到一个名为 'classes.jar' 的存档文件中:
jar cvfm classes.jar mymanifest -C foo/ .
4.3 javadoc
javadoc: 错误 - 未指定软件包或类。
用法:javadoc [选项] [软件包名称] [源文件] [@file]
-overview <文件> 读取 HTML 文件的概述文档
-public 仅显示公共类和成员
-protected 显示受保护/公共类和成员(默认)
-package 显示软件包/受保护/公共类和成员
-private 显示所有类和成员
-help 显示命令行选项并退出
-doclet <类> 通过替代 doclet 生成输出
-docletpath <路径> 指定查找 doclet 类文件的位置
-sourcepath <路径列表> 指定查找源文件的位置
-classpath <路径列表> 指定查找用户类文件的位置
-exclude <软件包列表> 指定要排除的软件包的列表
-subpackages <子软件包列表> 指定要递归装入的子软件包
-breakiterator 使用 BreakIterator 计算第 1 句
-bootclasspath <路径列表> 覆盖引导类加载器所装入的
类文件的位置
-source <版本> 提供与指定版本的源兼容性
-extdirs <目录列表> 覆盖安装的扩展目录的位置
-verbose 输出有关 Javadoc 正在执行的操作的消息
-locale <名称> 要使用的语言环境,例如 en_US 或 en_US_WIN
-encoding <名称> 源文件编码名称
-quiet 不显示状态消息
-J<标志> 直接将 <标志> 传递给运行时系统
通过标准 doclet 提供:
-d <目录> 输出文件的目标目录
-use 创建类和软件包用法页面
-version 包含 @version 段
-author 包含 @author 段
-docfilessubdirs 递归复制文档文件子目录
-splitindex 将索引分为每个字母对应一个文件
-windowtitle <文本> 文档的浏览器窗口标题
-doctitle <html 代码> 包含概述页面的标题
-header <html 代码> 包含每个页面的页眉文本
-footer <html 代码> 包含每个页面的页脚文本
-bottom <html 代码> 包含每个页面的底部文本
-link <url> 创建指向位于 <url> 的 javadoc 输出的链接
-linkoffline <url> <url2> 利用位于 <url2> 的软件包列表链接至位于 <url>
的文档
-excludedocfilessubdir <名称 1>:..排除带有给定名称的所有文档文件子目录。
-group <名称> <p1>:<p2>.. 在概述页面中,将指定的软件包分组
-nocomment 抑止描述和标记,只生成声明。
-nodeprecated 不包含 @deprecated 信息
-noqualifier <名称 1>:<名称 2>:...从输出中排除限定符的列表。
-nosince 不包含 @since 信息
-notimestamp 不包含隐藏时间戳
-nodeprecatedlist 不生成已过时的列表
-notree 不生成类分层结构
-noindex 不生成索引
-nohelp 不生成帮助链接
-nonavbar 不生成导航栏
-serialwarn 生成有关 @serial 标记的警告
-tag <名称>:<位置>:<标题> 指定单个变量自定义标记
-taglet 要注册的 Taglet 的全限定名称
-tagletpath Taglet 的路径
-charset <字符集> 用于跨平台查看生成的文档的字符集。
-helpfile <文件> 包含帮助链接所链接到的文件
-linksource 以 HTML 格式生成源
-sourcetab <制表符长度> 指定源中每个制表符占据的空格数
-keywords 使软件包、类和成员信息附带 HTML 元标记
-stylesheetfile <路径> 用于更改生成文档的样式的文件
-docencoding <名称> 输出编码名称
4.4 rmid
rmid: 非法选项:-?
用法:rmid <option>
其中,<option> 包括:
-port <option> 指定供 rmid 使用的端口
-log <directory> 指定 rmid 将日志写入的目录
-stop 停止当前的 rmid 调用(对指定端口)
-C<runtime 标记> 向每个子进程传递参数(激活组)
-J<runtime 标记> 向 java 解释程序传递参数
5. 常见问题分类
5.1 内存泄露
详见 JVM原理及优化之十: JVM内存泄漏专题
5.2 GC性能消耗高
- GC操作时间过长
- GC全量操作
5.3 JVM CPU 使用率高
以下是两个可能的原因:
- 复杂正则导致 CPU 使用率高
- HashMap 在并发访问下导致 CPU 使用率高
HashMap 是非线程安全的,在并发访问的情况下就可能出现死循环,这个死循环的分析网上很多了。 Spring 的缓存模块( spring-modules-cache-0.7.jar )用它作为缓存,在平时并发访问度不高,没有问题,被恶意扫描时,就触发了死循环
6. 问题定位
给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里说的数据包括:运行日志、异常堆栈、 GC 日志、线程快照( threaddump/javacore 文件)、堆转储快照( heapdump/hprof 文件)等。
7. 故障处理
7.1 案例1(线程死锁)
使用jconsole工具可以检测线程死锁,如下图:
7.2 案例2(异常日志问题)
问题描述:
生产环境抛异常 , 但却没有将堆栈信息输出到日志 , 可以确定的是日志输出时用的是 log.error("xx 发生错误 ", e)
问题分析:
它跟 JDK5 的一个新特性有关 , 对于一些频繁抛出的异常 ,JDK 为了性能会做一个优化 , 即 JIT 重新编译后会抛出没有堆栈的异常。
而在使用 -server 模式时 , 该优化选项是开启的 , 因此在频繁抛出某个异常一段时间后 , 该优化开始起作用 , 即只抛出没有堆栈的异常信息。
问题解决:
由于该优化是在 JIT 重新编译后才起作用 , 因此起初抛出的异常还是有堆栈的 , 所以可以查看较旧的日志 , 寻找完整的堆栈信息。
另一个解决办法是暂时禁用该优化 , 即强制要求每次都要抛出有堆栈的异常 , 幸好 JDK 提供了通过配置 JVM 参数的方式来关闭该优化。
即 -XX:-OmitStackTraceInFastThrow, 便可禁用该优化了 ( 注意选项中的减号 , 加号则表示启用 )
7.3 案例3(高CPU占用)
问题描述:
生产环境下的某台tomcat7服务器,在刚发布时的时候一切都很正常,在运行一段时间后就出现CPU占用很高的问题,基本上是负载一天比一天高。
问题分析:
-
程序属于CPU密集型,和开发沟通过,排除此类情况。
-
程序代码有问题,出现死循环,可能性极大。
问题解决:
-
开发那边无法排查代码某个模块有问题,从日志上也无法分析得出。
-
记得原来通过strace跟踪的方法解决了一台PHP服务器CPU占用高的问题,但是通过这种方法无效,经过google搜索,发现可以通过下面的方法进行解决,那就尝试下吧。
解决过程:
-
根据top命令,发现PID为2633的Java进程占用CPU高达300%,出现故障。
-
找到该进程后,如何定位具体线程或代码呢,首先显示线程列表,并按照CPU占用高的线程排序:
[root@localhost logs]# ps -mp 2633 -o THREAD,tid,time | sort -rn
显示结果如下:
USER %CPU PRI SCNT WCHAN USER SYSTEM TID TIME
root 10.5 19 - - - - 3626 00:12:48
root 10.1 19 - - - - 3593 00:12:16
找到了耗时最高的线程3626,占用CPU时间有12分钟了!
将需要的线程ID转换为16进制格式:
[root@localhost logs]# printf "%x\n" 3626
e18
最后打印线程的堆栈信息:
[root@localhost logs]# jstack 2633 |grep e18 -A 30
将输出的信息发给开发部进行确认,这样就能找出有问题的代码。
通过几天的监控,CPU已经安静下来了。
该专题是一个系列,参照了一系列JVM资料,对JVM基础知识做了摘要总结,并结合实战做了总结:
【基础+实战】JVM原理及优化系列之一:JVM体系结构
【基础+实战】JVM原理及优化系列之二:JVM内存管理
【基础+实战】JVM原理及优化系列之三:JVM垃圾收集器
【基础+实战】JVM原理及优化系列之四:JVM参数说明
【基础+实战】JVM原理及优化系列之五:JVM默认设置
【基础+实战】JVM原理及优化系列之六:JVM主要调优参数
【基础+实战】JVM原理及优化系列之七:JVM调优注意事项
【基础+实战】JVM原理及优化系列之八:如何查看JVM参数配置?
【基础+实战】JVM原理及优化系列之九:JVM监控、分析与故障处理实战
【基础+实战】JVM原理及优化系列之十:JVM内存泄漏专题实战
通览该系列文章之后,对JVM会有一个整体的认识,对于JVM问题排查和优化会有一定的帮助,如果想对JVM有更深入的理解和认知,建议深入看一下这本书《Java虚拟机:JVM高级特性与最佳实践(最新第二版)》,网上可以找到pdf版的,大家可以自己百度一下。