【基础+实战】JVM原理及优化系列之九:JVM监控、分析与故障处理实战

1. 监控工具

  1. jvisualvm(JDK内置)

  2. jconsole(JDK内置)

  3. jmc(JDK内置)

  4. Jprofile(第三方)

  5. Eclipse Memory Analyzer

  6. 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 6root@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 7S0C、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 3jmap [option] pid jmap [option] executable core jmap [option] [server-id@]remote-hostname-or-ip

1

1jmap -permstat pid

1

打印进程的类加载器和类加载器加载的持久代对象信息,输出:类加载器名称、对象是否存活(不可靠)、对象地址、父类加载器、已加载的类大小等信息

使用jmap -heap pid 查看进程堆内存使用情况,包括使用的 GC 算法、堆配置参数和各代中堆内存使用情况。

使用jmap -histo[:live] pid   查看堆内存中的对象数目、大小统计直方图,如果带上 live 则只统计活对象

  还有一个很常用的情况是:用 jmap 把进程内存使用情况 dump 到文件中,再用 jhat 分析查看。 jmap 进行 dump 命令格式如下:

1jmap -dump:format=b,file=dumpFileName

1

1 2 3root@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 10root@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性能消耗高

  1. GC操作时间过长
  2. GC全量操作

5.3 JVM CPU 使用率高

以下是两个可能的原因:

  1. 复杂正则导致 CPU 使用率高
  2. 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占用很高的问题,基本上是负载一天比一天高。

问题分析:

  1. 程序属于CPU密集型,和开发沟通过,排除此类情况。

  2. 程序代码有问题,出现死循环,可能性极大。

问题解决:

  1. 开发那边无法排查代码某个模块有问题,从日志上也无法分析得出。

  2. 记得原来通过strace跟踪的方法解决了一台PHP服务器CPU占用高的问题,但是通过这种方法无效,经过google搜索,发现可以通过下面的方法进行解决,那就尝试下吧。

解决过程:

  1. 根据top命令,发现PID为2633的Java进程占用CPU高达300%,出现故障。

  2. 找到该进程后,如何定位具体线程或代码呢,首先显示线程列表,并按照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版的,大家可以自己百度一下。

代码交流 2021