【基础+实战】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 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
1

要明白上面各列的意义,先看
JVM
堆内存布局:

1 2 堆内存 = 年轻代 + 年老代 + 永久代 年轻代 = Eden区 + 两个Survivor区(From和To)

1
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
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
1 jmap -permstat pid

1
1

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

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

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

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

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

1
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
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
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
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版的,大家可以自己百度一下。

给TA打赏
共{{data.count}}人
人已打赏
安全技术

详解Node.js API系列 Crypto加密模块(2) Hmac

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索