【深入理解JVM虚拟机】第4章 虚拟机性能监控与故障处理工具

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/CharJay_Lin/article/details/84453154

4.1.JDK命令行工具

4.1.1 jps:虚拟机进程状况工具

jps命令可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class, main()函数所在类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier, LVMID)。如果只启动了一台本地虚拟机,那么LVMID和系统PID是相同的。

jps命令格式:jps [options] [hostid]

当我没有运行任何java程序时运行jps -l,这时只有一条JVM记录:

nobody$ jps -l
2305 sun.tools.jps.Jps

当我执行下面的程序时,在这个程序里我让程序一直不结束。

public static void main(String args[]) {
    new Thread(new Runnable(){
        @Override
        public void run() {
            while (true) {
                try {
                    System.out.println("Hello");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
}

这时再运行jps -l参数,会看到多了一个JVM进程:

nobody$ jps -l
2311 com.intellij.rt.execution.application.AppMain
2312 sun.tools.jps.Jps

除了使用-l参数输出主类的全名外,jps还有以下其他参数:

选项 作用
-q 只输出LVMID,省略主类的名称
-m 输出虚拟机进程启动时传递给主类main()函数的参数
-l 输出主类的全名,如果进程执行的是Jar包,输出Jar路径
-v 输出虚拟机进程启动时JVM参数

jps还可以通过RMI协议查询启动了RMI服务的远程虚拟机进程状态,hostid为RMI注册表中注册的主机名。

4.1.2 jstat:虚拟机统计信息监视工具

jstat是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行参数。

jsat命令格式为:jstat [option vmid [interval[s|ms] [count]]]

对于命令格式中的VMID与LVMID需要特别说明一下:如果是本地虚拟机进程,VMID和LVMID是一致的,如果是远程虚拟机进程,那VMID的格式应当是:

[protocol:] [//] lvmid [@hostname[:port]/servername]

参数interval和count代表查询间隔和次数,如果省略这两个参数,说明只查询一次。假设需要每250毫秒查询一次进程2764垃圾收集状况,一共查询20次,那命令应当是:

jstat -gc 2764 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编译的方法

下面我们用jstat命令每秒监视一个LVMID为2365的JVM进程

YuRongChandeMacBook-Pro:insightjvm_notebook yurongchan$ jstat -gcutil 2365 1000 
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT   
  0.00   0.00  12.05   0.00  14.17      0    0.000     0    0.000    0.000

其中每个选项的意义如下:

  • S0、S1:表示Survivor0、Survivor1,还未使用。
  • E:表示Eden区使用了12.05%的空间。
  • O:表示老年代还未使用。
  • P:表示永久代使用了14.17%的空间
  • YUC、YGCT:表示从程序运行以来一共发生了0次Minor GC(YGC,Young GC),总共耗时0秒。
  • FGC、FGCT:表示从程序运行以来一共发生了0次Full GC(FGC,Full GC),总共耗时0秒。

4.1.3 jinfo:Java配置信息工具

jinfo可以用于实时地查看和调整虚拟机各项参数。其实使用jps -v可以查看虚拟机启动时显式指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,还可以使用jinfo的-flag选项。

jinfo命令格式:jinfo [option] pid

执行例子,查询 CMSInitiatingOccupancyFraction 参数值

nobody$ jinfo -flag CMSInitiatingOccupancyFraction 2618
-XX:CMSInitiatingOccupancyFraction=-1

4.1.4 jmap:java内存映像工具

jmap 命令用于生成堆转储快照。

jmap命令格式:jmap [option] vmid

执行样例,使用 jmap 生成一个正在运行的 Eclipse 的 dump 快照文件的例子。例子中的2618是通过jps名称查询到的LVMID。

$ jmap -dump:format=b,file=Desktop/dump.bin 2618
Dumping heap to /Users/yurongchan/Desktop/dump.bin ...
Heap dump file created

jmap工具主要选项

选项 作用 适用系统
-dump 生成Java堆转储快照。格式为:-dump[live, ]format=b, file=,其中live子参数说明是否只 dump 出存活的对象 所有系统
-histo 显示堆中对象统计信息,包括类、实例数量、合计容量 所有系统
-finalizerinfo 显示在F-Queue中等待Finalizer线程执行finalize方法的对象。 Linux/Solaris
-heap 显示Java堆详细信息,如使用哪种回收器、参数配置、分代状况等 Linux/Solaris
-permstat 以ClassLoader为统计口径显示永久代内存装 Linux/Solaris
-F 当虚拟机进程对-dump选项没有响应时,可使用这个选项强制生成dump快照 Linux/Solaris

NOTE:

  • 通过-XX:+HeapDumpOnOutOfMemoryError参数可以让虚拟机在OOM异常出现之后自动生成dump文件。
  • 通过-XX:+HeapDumpOnCtrlBreak参数则可以使用[Ctrl]+[Break]键让虚拟机生成dump文件
  • 在Linux系统下通过kill -3命令发送进程退出信号也可以生成dump文件

4.1.5 jhat:虚拟机堆转存储快照分析工具

Sun JDK 提供了 jhat 命令与 jmap 搭配使用,来分析 jmap 生成的堆转储快照。jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,可以在浏览器中查看。下面我们用 jhat 来分析上面生成的 dump.bin 文件:

$ jhat dump.bin 
Reading from dump.bin...
Dump file created Sun May 15 23:04:19 CST 2016
Snapshot read, resolving...
Resolving 13822 objects...
Chasing references, expect 2 dots..
Eliminating duplicate references..
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

屏幕系那是“Server is ready”后,在浏览器输入 http://localhost:7000 就可以看到分析结果,如下图所示:

04_01_01_jhat_browser.jpg | left | 827x488

不过一般情况下不用 jhat 命令来分析 dump 文件,主要有以下两个原因:一是一般不会再部署应用的服务器上分析 dump 文件,因为分析工作是一个耗时而且消耗硬件资源的过程。另一个原因是 jhat 的分析功能还比较简陋,比起后面介绍的 VisualVM 等工具还差得很多。

NOTE:按下Ctrl+Z可以停止。

4.1.6 jstack:Java堆栈跟踪工具

jstack 命令用于生成虚拟机当前时刻的线程快照(一般称为 threaddump 或者 Javacore 文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。

jstack命令格式:jstack [option] vmid

下面使用jstack查看一个线程对战的例子,

nobody $ jstack -l 2618
2016-05-15 23:39:04
Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):
"Attach Listener" daemon prio=5 tid=0x00007f83228e6000 nid=0x280b waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
	- None
"DestroyJavaVM" prio=5 tid=0x00007f832387e800 nid=0x1303 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE
   Locked ownable synchronizers:
	- None

jstack 工具主要选项如下:

选项 作用
-F 当正常输出的请求不被响应时,强制输出线程对战
-l 除堆栈外,显示关于锁的附加信息
-m 如果调用到本地方法的话,可以显示C/C++的堆栈

4.1.7 HSDIS:JIT生成代码反汇编

HSDIS 是一个 Sun 官方推荐的 HotSpot 虚拟机 JIT 编译代码的反汇编插件,它的作用是让 HotSpot 的 -XX:+PrintAssembly 指令调用它来把动态生成的本地代码还原为汇编输出。

HSDIS 包含在 HotSpot 虚拟机的源码之中,但没有提供编译后的程序。读者可以根据自己的操作系统和 CPU 类型在 Project Kenai 的网站下载到编译好的插件,直接放到 JDK_HOME/jre/bin/clientJDK_HOME/jre/bin/server 目录中即可。如果没有找到,则需要自己用源码编译一下。

但是如果你使用的是 Debug 或者 FastBug 版的 HotSpot,那可以直接通过 -XX:PrintAssembly 指令使用插件。如果使用的是 Product 版的 HotSpot,那还需要额外加入一个 -XX:UnlockDiagnosticVMOptions 参数。

NOTE:补充一个实例

4.2.JDK的可视化工具

JDK中除了提供大量的明命令行工具外,还提供了两个强大的可视化工具:JConsole 和 VisualVm。

其中 JConsole 是在 JDK1.5 时期就已经提供的虚拟机监控工具,而 VisualVM 在 JDK 1.6 Update7 中才首次发布。

4.2.1 JConsole:Java监视与管理控制台

JConsole 是一种基于 JMX 的可视化监视、管理工具。

启动 JConsole

通过 JDK/bin 目录下的 jconsole.exe 启动 JConsole 后,将自动搜索出本机运行的所有虚拟机进程,不需要用户自己通过 jps 命令来查询。双击其中一个进程即可开始监控也可以使用下面的“远程进程”功能来连接远程服务器,对远程虚拟机进程监控。

04_02_01_jconsole_startup.png | left | 827x704

JConsole 主界面一共有“概述”、“内存”、“线程”、“类”、“VM摘要”、“MBean”6个也签,如图所示:

04_02_02_jconsole_overview.png | left | 827x704

“概述”页签显示的是整个虚拟机主要运行数据的概览,其中包括“堆内存使用情况”、“线程”、“类”、“CPU使用情况”4种信息的曲线图,这些曲线图后面是“内存”、“线程”、“类”页签的信息汇总,具体内容后面详细介绍。

内存监控

“内存”页签相当于可视化的 jstat 命令,用于监视受可视化参数管理的虚拟机内存(Java堆和永久代)的变化趋势。我们通过下面的代码来体验一下它的监视功能。运行时设置的虚拟机参数为:-Xms100m -Xmx100m -XX:+UseSerialGC,这块代码的作用是以 64KB/50毫秒 的速度往 Java 堆中填充数据,一共填充1000次,使用 JConsole 的“内存”页进行监视,观察曲线和柱状指示图的变化。

/**
 * 内存占位符对象,一个 OOMObject 大约占 64KB
 */
static class OOMObject{
    public byte[] placeholder = new byte[64 * 1024];
}
public static void fillHeap(int num) throws InterruptedException {
    List<OOMObject> list = new ArrayList<OOMObject>();
    for (int i = 0; i < num; i++) {
        //稍作延时,令监视曲线的变化更加明显
        Thread.sleep(50);
        list.add(new OOMObject());
    }
    System.gc();
}
public static void main(String args[])throws Exception {
    fillHeap(1000);
}

运行程序后可以发现整个堆是一条向上增长的平滑曲线。

04_02_03_jconsole_memory_1.png | left | 827x689

当我们把监视区域缩小至Eden区时会发现其是一条折线图。从图中可以看出整个Eden区的大小是27328KB,默认情况下Eden:Survivor=8:1,所以整个新生代的大小是27328*125%=34160KB。当Eden区内存不足时,会执行GC,执行GC后Eden区被清空,所以Eden区才是折线图。

04_02_03_jconsole_memory_2.png | left | 827x689

当我们把监视区域切换至老年代时,会发现其仍然是平稳上升并到达了一个顶点后稳定下去。

04_02_03_jconsole_memory_3.png | left | 827x689

线程监控

“线程”页签的功能相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析。之前说过导致线程长时间停顿的主要原因有:等待外部资源、死循环、锁等待等。下面我们通过代码来演示一下。

/**
 * 线程死循环演示
 */
public static void createBusyThread() {
    new Thread(new Runnable() {
        @Override
        public void run() {
            int i = 1;
            while (true) {
                i++;
                System.out.println("Hello" + i);
            }
        }
    }, "testBusyThread").start();
}
/**
 * 测试锁等待演示
 * @param lock
 */
public static void createLockThread(final Object lock) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }, "testLockThread").start();
}
public static void main(String args[]) throws Exception {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    br.readLine();
    createBusyThread();
    br.readLine();
    createLockThread(new Object());
}

运行程序后在“线程”页签中会发现只有main线程,其处于RUNNABLE状态,并且main线程在等待输入。当输入字符按下回车键之后,会多了一个testBusyThread的线程,并且显示其处于RUNNABLE状态,且一直在执行createBusyThread()方法22行处的代码 while(true)。这种死循环很耗CPU资源,在Windows系统下一不小心就会造成系统死机。

04_02_04_jconsole_thread_1.png | left | 827x558

当我们再次往控制台输入字符并按下回车键时,会发现又多了一个testLockThread线程,显示testLockThread线程处于WAITING状态,并且处于代码第38行,即:lock.wait() 处。这种等待方式是正常的等待方式,只要有另外一个地方调用了 notify() 或 notifyAll() 方法,线程就有机会得以运行。

04_02_04_jconsole_thread_2.png | left | 827x558

下面的代码演示了一个无法被激活的死锁等待。

public static void main(String args[]) throws Exception {
    for (int i = 0; i < 100; i++) {
        new Thread(new SynAddRunnable(1, 2)).start();
        new Thread(new SynAddRunnable(2, 1)).start();
    }
}
static class SynAddRunnable implements Runnable {
    int a,b;
    public SynAddRunnable(int a, int b) {
        this.a = a;
        this.b = b;
    }
    public void run() {
        synchronized (Integer.valueOf(a)) {
            synchronized (Integer.valueOf(b)) {
                System.out.println(a + b);
            }
        }
    }
}

上面的代码因为Integer.valueOf()创建对象的原因,只存在一个new Integer(1)和一个new Integer(2)对象,因此会出现200个线程进行资源争夺的情况。这时候按下“Detect DeadLock”按钮可以检测是否有死锁存在,如果有死锁存在那么会新打开一个页签,如下图所示:

04_02_04_jconsole_thread_3.png | left | 827x704

从上图可以看出Thread-199处于阻塞状态,原因是其中有一个锁被Thread-11持有,而Thread-11锁需要的锁又被Thread-4持有,Thread-4所需要的锁又被Thread-199持有,它们一起形成了一个死锁环。

4.2.2 VisualVM:多合一故障处理工具

jvisualvm

猜你喜欢

转载自blog.csdn.net/CharJay_Lin/article/details/84453154
今日推荐