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 | 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性能消耗高
- 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版的,大家可以自己百度一下。