给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里说的数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)等。经常使用适当的虚拟机监控和分析的工具可以加快我们分析数据、定位解决问题的速度,今天来好好学习一下这些工具。
常用的命令行工具主要有 jps、jstat、jinfo、jmap、jhat、jstack。
jps 可以列出正在运行的虚拟机进程,并显示虚拟机执行的主类名称以及这些进程的本地虚拟机唯一ID。
jps 工具主要选项 选项 | 作用 :-- | :-- -q | 只输出 LVMID,省略主类的名称 -m | 输出虚拟机进程启动时传递给主类 main() 函数的参数 -l | 输出主类的全名,如果进程执行的是 jar 包,输出包的路径 -v | 输出虚拟机进程启动时的 JVM 参数
例子:
jstat 是用于监视虚拟机各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程的类装载、内存、垃圾收集、JIT 编译等运行数据,缺点是没有 GUI 界面,只提供纯文本控制台环境的服务器,是运行期定位虚拟机性能问题的首选工具。
命令格式为:
jstat [option vmid[interval[s|ms][count]]]
参数 interval 和 count 代表查询间隔和次数,如果省略这两个参数,说明只查询一次。
假设需要每 250 毫秒查询一次进程 12540 垃圾收集状况,一共查询 20 次,那命令应当是:
jstat -gc 12540 250 20
效果如下:
选项 option 代表着用户希望查询的虚拟机信息,主要分为3类:类装载、垃圾收集、运行期编译状况,具体选项及作用见下表。
选项 | 作用 |
---|---|
-class | 监视类装载、卸载数量、总空间以及类装载所耗费的时间 |
-gc | 监视 Java 堆状况,包括 Eden 区、两个 Survivor 区、老年代、永久代等的容量,已用空间、GC 时间合计等信息 |
-gccapacity | 监视内容与 -gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间 |
-gcutil | 监视内容与 -gc 基本相同,但输出主要关注已使用的空间占总空间的百分比 |
-gccause | 与 -gcutil 功能一样,但会额外输出导致上一次 GC 产生的原因 |
-gcnew | 监视新生代 GC 状况 |
-gcnewcapacity | 监视内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间 |
-gcold | 监视老年代 GC 状况 |
-gcoldcapacity | 监视内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间 |
-gcpermcapacity | 输出永久代使用的最大、最小空间 |
-compiler | 输出 JIT 编译器编译过得方法耗时等信 |
-printcompilation | 输出已经被 JIT 编译的方法 |
还有很多没有列举出来,下次用到再加进来
例子:
以上查询结果表明:这台服务器的新生代 Eden 区(E,表示 Eden)使用了 55.68% 的空间,两个 Survivor 区(S0、S1,表示 Survivor0、Survivor1)里面 s1是空的,s0 使用了 48.99% 的空间,老年代(O,表示Old)和永久代(P,表示Permanent)则分别使用了 76.58% 和 93.57% 的空间。程序运行以来共发生 Minor GC(YGC,表示Young GC)1226 次,总耗时 13.524 秒,发生Full GC(FGC,表示Full GC)13 次,Full GC 总耗时(FGCT,表示Full GC Time)为 2.692 秒,所有GC总耗时(GCT,表示GC Time)为 16.215 秒。
jstat 挺好用的,后面要多多学习和使用。
jinfo 的作用是实时的查看和调整虚拟机各项参数。
jmap 命令用于生成堆转储快照。当热,jmap 的作用并不仅仅是为了获取 dump 文件,它还可以查询 finalize 执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。
如果不使用 jmap 命令,要想获取 Java 堆转储快照,还有一些比较“暴力”的手段:譬如启动时加入 -XX:+HeapDumpOnOutOfMemoryError 参数,可以让虚拟机在 OOM 异常出现之后自动生成 dump 文件,通过 -XX:+HeapDumpOnCtrlBreak 参数则可以使用 [Ctrl]+[Break] 键让虚拟机生成 dump 文件,又或者在 Linux 系统下通过 Kill-3 命令发送进程退出信号“吓唬”一下虚拟机,也能拿到 dump 文件。
jmap 命令格式:
jmap [option] vmid
option 选项枚举值说明:
选项 | 作用 |
---|---|
-dump | 生成 Java堆转储快照。格式为 -dump:[live, ]format=b, file=, 其中 live 子参数说明是否只 dump 出存活的对象 |
-finalizerinfo | 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux / Solaris 平台有效 |
-heap | 显示 Java 堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在 Linux / Solaris 平台有效 |
-histo | 显示堆中对象统计信息,包括类、实例数量、合计容量 |
-permstat | 以 ClassLoader 为统计口径显示永久代内存状态。只在 Linux / Solaris 平台有效 |
-F | 当虚拟机进程对 -dump 选项没有响应时,可使用这个选项强制生成 dump 快照。只在 Linux / Solaris 平台有效 |
jmap -dump:format=b,file=123.txt 2088199
jmap -heap 2088199
Sun JDK 提供 jhat(JVM Heap Analysis Tool)命令与 jmap 搭配使用,来分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,可以在浏览器中查看。
一般都不会去直接使用 jhat 命令来分析 dump 文件,主要原因有二:一是一般不会在部署应用程序的服务器上直接分析 dump 文件,即使可以这样做,也会尽量将 dump 文件复制到其他机器上进行分析,因为分析工作是一个耗时而且消耗硬件资源的过程,既然都要在其他机器进行,就没有必要受到命令行工具的限制了;另一个原因是 jhat 的分析功能相对来说比较简陋,还有诸如 VisualVM,以及专业用于分析 dump 文件的Eclipse Memory Analyzer、IBM HeapAnalyzer 等工具,都能实现比 jhat 更强大更专业的分析功能。
屏幕显示 “Server is ready.” 的提示后,用户在浏览器中键入 http://localhost:7000/ 就可以看到分析结果。
jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为 threaddump 或者 javacore 文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过 jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack 命令格式:
jstack [option] vmid
option 选项枚举值及作用说明: 选项 | 作用 :-- | :-- -F | 当正常输出的请求不被响应时,强制输出线程堆栈 -l | 除堆栈外,显示关于锁的附加信息 -m | 如果调用到本地方法的话,可以显示 C/C++ 的堆栈
这个很少使用
HSDIS 是一个 Sun 官方推荐的 HotSpot 虚拟机 JIT 编译代码的反汇编插件,它包含在 HotSpot 虚拟机的源码之中,但没有提供编译后的程序。
它的作用是让 HotSpot 的 -XX:+PrintAssembly 指令调用它来把动态生成的本地代码还原为汇编代码输出,同时还生成了大量非常有价值的注释,这样我们就可以通过输出的代码来分析问题。
JDK 中除了提供大量的命令行工具外,还有两个功能强大的可视化工具:JConsole 和 VisualVM。
JConsole 在 JDK 1.5 时就提供,而 VisualVM 是在 JDK 1.6 Update7 才发布的。
JConsole 是一直基于 JMX 的可视化监视、管理工具。它管理部分的功能是针对 JMX MBean 进行管理,有 MBean 可以使用代码、中间件服务器的管理控制台或者所有符合 JMX规范的软件进行访问。
通过 JDK/bin 目录下的 “jconsole.exe” 启动 JConsole 后,将自动搜索出本机运行的所有虚拟机进程,不需要用户自己再使用 jps 来查询了。双击选择其中一个进程即可开始监控,也可以使用下面的“远程进程”功能来连接远程服务器,对远程虚拟机进行监控。
连接后可以看到:
“概述”页签显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用情况”、“线程”、“类”、“CPU使用情况”4种信息的曲线图,这些曲线图是后面“内存”、“线程”、“类”页签的信息汇总。
“内存”页签相当于可视化的 jstat 命令,用于监视受收集器管理的虚拟机内存(Java 堆和永久代)的变化趋势。
可以看到内存池 Eden 区的运行趋势呈现折线状,而监视范围扩大至整个堆后,会发现曲线是一条向上增长的平滑曲线。
如果上面的“内存”页签相当于可视化的 jstat 命令的话,“线程”页签的功能相当于可视化的 jstack 命令,遇到线程停顿时可以使用这个页签进行监控分析。前面讲解 jstack 命令的时候提到过线程长时间停顿的主要原因主要有:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待(活锁和死锁)。
线程等待演示代码:
public class TestThread {
public static void createBusyThread() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
;
}
}
}, "testBusyThread");
thread.start();
}
public static void createLockThread(final Object lock) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "testLockThread");
thread.start();
}
public static void main(String[] args) throws Exception{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
br.readLine( );
createBusyThread();
br.readLine();
Object obj = new Object();
createLockThread(obj);
}
}
程序运行后,首先在“线程”页签中选择main线程,如下图所示。堆栈追踪显示 BufferedReader 在 readBytes 方法中等待 System.in 的键盘输入,这时线程为 Runnable 状态,Runnable 状态的线程会被分配运行时间,但 readBytes 方法检查到流没有更新时会立刻归 还执行令牌,这种等待只消耗很小的 CPU 资源。
接着监控 testBusyThread 线程,如下图所示,testBusyThread 线程一直在执行空循环。这时候 线程为 Runnable 状态,而且没有归还线程执行令牌的动作,会在空循环上用尽全部执行时间 直到线程切换,这种等待会消耗较多的 CPU 资源。
显示 testLockThread 线程在等待着 lock 对象的 notify 或 notifyAll 方法的出现,线程这时 候处于 WAITING 状态,在被唤醒前不会被分配执行时间。
testLockThread 线程正在处于正常的活锁等待,只要 lock 对象的 notify() 或 notifyAll() 方法被调用,这个线程便能激活以继续执行。
死锁代码演示:
public class TestLock {
static class SynAddRunnable implements Runnable {
int a,b;
public SynAddRunnable(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
synchronized (Integer.valueOf(a)) {
synchronized(Integer.valueOf(b)) {
System.out.println(a + b);
}
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(new SynAddRunnable(1, 2)).start();
new Thread(new SynAddRunnable(2, 1)).start();
}
}
}
这段代码开了 200 个线程去分别计算 1+2 以及 2+1 的值,其实 for 循环是可省略的,两个线程也可能会导致死锁,不过那样概率太小,需要尝试运行很多次才能看到效果。一般的话, 带 for 循环的版本最多运行 2~3 次就会遇到线程死锁,程序无法结束。造成死锁的原因是 Integer.valueOf() 方法基于减少对象创建次数和节省内存的考虑,[-128,127] 之间的数字会 被缓存,当 valueOf() 方法传入参数在这个范围之内,将直接返回缓存中的对象。也就是说,代码中调用了 200 次 Integer.valueOf() 方法一共就只返回了两个不同的对象。假如在某个线程的两个 synchronized 块之间发生了一次线程切换,那就会出现线程 A 等着被线程 B 持有的 Integer.valueOf(1) ,线程 B 又等着被线程 A 持有的 Integer.valueOf(2) ,结果出现大家都 跑不下去的情景。
出现线程死锁之后,点击JConsole线程面板的“检测到死锁”按钮,将出现一个新的“死 锁”页签,如下图所示。
很清晰地显示了线程 Thread-3 在等待一个被线程 Thread-4 持有 Integer 对象,而点击线程 Thread-4 则显示它也在等待一个 Integer 对象,被线程Thread-3 持有,这样两个线程就互相卡住,都不存在等到锁释放的希望了。
VisualVM(All-in-One Java Troubleshooting Tool)是到目前为止随JDK发布的功能最强大的运行监视和故障处理程序。
优点:不需要被监视的程序基于特殊 Agent 运行,因此它对应用程序的实际性能的影响很小,使得它可以直接应用在生产环境中。
VisualVM 基于 NetBeans 平台开发,因此它一开始就具备了插件扩展功能的特性,通过插件扩展支持,VisualVM 可以做到:
在 VisualVM 中生成 dump 文件有两种方式,可以执行下列任一操作:
生成了 dump 文件之后,应用程序页签将在该堆的应用程序下增加一个以[heapdump] 开头的子节点,并且在主页签中打开了该转储快照。如果需要把dump 文件保存或发送出去,要在 heapdump 节点上右键选择“另存为”菜单,否则当 VisualVM 关闭时,生成的 dump 文件会被当做临时文件删除掉。要打开一个已经存在的 dump 文件,通过文件菜单中的“装入”功能,选择硬盘上的 dump 文件即可。
在 Profiler 页签中,VisualVM 提供了程序运行期间方法级的 CPU 执行时间分析以及内存分析,做Profiling分析肯定会对程序运行性能有比较大的影响,所以一般不在生产环境中使用这项功能。
要开始分析,先选择 “CPU” 和“内存”按钮中的一个,然后切换到应用程序中对程序进行操作,VisualVM 会记录到这段时间中应用程序执行过的方法。如果是 CPU 分析,将会统计每个方法的执行次数、执行耗时;如果是内存分析,则会统计每个方法关联的对象数以及这些对象所占的空间。分析结束后,点击“停止”按钮结束监控过程,
BTrace 是一个很“有趣”的 VisualVM 插件,本身也是可以独立运行的程序。它的作用是在不停止目标程序运行的前提下,通过 HotSpot 虚拟机的 HotSwap 技术动态加入原本并不存在的调试代码。这项功能对实际生产中的程序很有意义:经常遇到程序出现问题,但排查错误的一些必要信息,譬如方法参数、返回值等,在开发时并没有打印到日志之中,以至于不得不停掉服务,通过调试增量来加入日志代码以解决问题。当遇到生产环境服务无法随便停止时,缺一两句日志导致排错进行不下去是一件非常郁闷的事情。 BTrace 的用法有许多,打印调用堆栈、参数、返回值只是最基本的应用,在它的网站上有使用 BTrace 进行性能监视、定位连接泄漏和内存泄漏、解决多线程竞争问题等例子。
这个后面再学习。。。
Copyright: 采用 知识共享署名4.0 国际许可协议进行许可 Links: https://lixj.fun/archives/虚拟机性能监控与故障处理工具