(七)虚拟机性能监控与故障处理工具

1.概述
给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里所说的数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)等。
2.JDK的命令行工具

名称 主要作用
jps JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程
jstat JVM Statistics Monitoring Tool,用于收集HotSpot虚拟机各方面的运行数据
jinfo Configuration Info for Java,显示虚拟机配置信息
jmap Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件)
jhat JVM Heap Dump Browser,用于分析和heapdump文件
jstack Stack Trace for Java,显示虚拟机的线程快照

2.1.虚拟机进程状况工具:jps
功能:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(Local Virtual Machine Identifier,LVMID)。
命令格式:jps [options] [hostid]

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

2.2.虚拟机统计信息监视工具:jstat
功能:可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、JIT编译等运行数据。
命令格式:jstat - [-t] [-h] [ [ ] ]

  • 对于命令格式中的VMID与LVMID需要特别说明一下:如果是本地虚拟机进程,VMID与LVMID是一致的;如果是远程虚拟机进程,那么VMID的格式应当是:
    [protocol:][//]lvmid[@hostname[:port]/servername]
  • -t:可以在打印的列加上Timstamp列,用于显示系统的运行时间
  • -h:可以在指定输出N行后输出一次表头
  • 参数interval和count代表查询间隔和次数,如果省略了这两个参数,说明只查询一次。
选项 作用
-class 监视类装载、卸载数量、总空间以及类装载所耗费的时间
-gc 监视Java堆状况、包括Eden区、两个Survivor区、老年代、永久代等的容量、已用空间、GC时间合计等信息
-gccapacity 监视内容与-gc基本相同,但输出主要关注Java堆各个区域使用到的最大和最小空间
-gcutil 监视内容与-gc基本相同,但输出主要关注已使用空间占总空间的百分比
-gccause 与-gcutil功能一样,但是会额外输出导致上一次GC产生的原因
-gcnew 监视新生代GC状况
-gcnewcapacity 监视内容与-gcnew基本相同,但输出主要关注使用到的最大和最小空间
-gcold 监视老年代GC状况
-gcoldcapacity 监视内容与-gcold基本相同,但输出主要关注使用到的最大和最小空间
-gcpermcapacity 输出永久代使用到的最大和最小空间(JDK1.8以后被-gcmetacapacity输出元数据空间使用到的最大和最小空间取代)
-compiler 输出JIT编译器编译过的方法、耗时等信息
-printcompilation 输出已经被JIT编译过的方法

2.3.Java配置信息工具:jinfo
功能:实时地查看和调整虚拟机各项参数
命令格式:jinfo [option] pid

选项 作用
-flag 打印指定的JVM参数值,例如:jinfo SurvivorRatio 8232
-flag[+/-] 使指定的JVM参数生效或失效,例如:jinfo -flag -printGCDateStamps 8232
-flag= 为指定的VM参数设定指定的值,例如:jinfo -flag MaxHeapFreeRatio=80 8232
-flags 打印所有的VM参数,例如:jinfo -flags 8232
-sysprops 打印系统参数,例如 jinfo -sysprops 8232

2.4.Java内存映像工具:jmap
功能:可用于生成堆转储快照(heapdump或dump文件),也可用于查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。
命令格式:jmap [option] vmid

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

2.5.虚拟机堆转储快照分析工具:jhat
功能:jhat(JVM Heap Analysis Tool)用于分析jmap生成的堆转储快照文件。
命令格式:jhat

2.6.Java堆栈跟踪工具:jstack
功能:用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因
命令格式:jstack [option] vmid

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

3.JDK的可视化工具
3.1.Java监视与管理控制台:JConsole
JConsole(Java Monitoring and Management Console)是一种基于JMX的可视化监视、管理工具。
3.1.1.启动JConsole
通过JDK/bin目录下的jconsole.exe启动JConsole后,将自动搜索出本机运行的所有虚拟机进程,不需要用户自己再使用jps来查询了。

JConsole连接页面.png
JConsole主界面.png

如上图所示,主界面共包括“概述”、“内存”、“线程”、“类”、“VM概要”和“MBean”6个页签。
3.1.2.内存监控
“内存”页签相当于可视化的jstat命令,用于监视受收集器管理的虚拟机内存(Java堆、永久代/元空间)的变化趋势。我们通过下面的一段代码来演示一下JConsole是如何监控内存变化的。

package com.nwpu.davince.jvm;

import java.util.ArrayList;
import java.util.List;

public class TestDemo {
    
    static class OOMObject {
        public byte[] placeHolder = new byte[64 * 1024];
    }

    private static void fillHeap(int num) throws InterruptedException {
        List list = new ArrayList();
        for (int i = 0; i < num; i++) {
            Thread.sleep(50);
            list.add(new OOMObject());
        }
        System.gc();
    }

    public static void main(String[] args) throws InterruptedException {
        fillHeap(1000);
    }
}
Eden区内存变化状况.png

程序运行后,可以看到内存池Eden区的运行趋势呈现折线状。而监视范围扩大至整个堆后,会发现曲线是一条向上增长的平滑曲线。

堆内存使用量.png

3.1.3.线程监控
“线程”页签的功能相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析。线程停顿的主要原因有:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待(活锁和死锁)。我们将通过以下代码来分别演示这几种情况。

package com.nwpu.davince.jvm;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class JStackDemo {

    /**
     * 线程死循环
     */
    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 IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        br.readLine();
        createBusyThread();
        br.readLine();
        Object lock = new Object();
        createLockThread(lock);
    }

}

程序运行后,首先在“线程”页签中选择main线程。堆栈追踪显示BufferReader在readBytes方法中等待System.in的键盘输入,这是线程为Runnable状态,Runnable状态的线程会被分配CPU运行时间,但readBytes方法检查到流没有更新时会立刻归还执行令牌,这种等待只消耗很小的CPU资源。

main线程.png

接着监控testBusyThread线程,testBusyThread线程一直在执行空循环,从堆栈追踪中看到一直在JStackDemo.java代码的16行停留,16行行为:while(true)。这时线程为Runnable状态,而且没有归还线程执行令牌的动作,会在空循环上用尽全部CPU执行时间直到线程切换,这种等待会消耗较多的CPU资源。

testBusyThread线程.png

testLockThread线程在等待着lock对象的notify或notifyAll方法的出现,线程这时候处于WAITING状态,在被唤醒前不会被分配执行时间。testLockThread线程在正处于正常的活锁等待,只要lock对象的notify或notifyAll方法被调用,这个线程便能激活以继续执行。

testLockThread线程.png

最后,我们来演示一个无法再被激活的死锁等待的例子。

package com.nwpu.davince.jvm;

public class ThreadDeadLock {

    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();
        }
    }
}

上述程序将会出现死锁等待的情况。而造成死锁的原因是Integer.valueOf()方法基于减少对象创建次数和节约内存的考虑,[-128,127]之间的数字会被缓存,当valueOf()方法传入这个参数在这个范围之内,将直接返回缓存中的对象。也就是说,代码中调用了200次Integer.valueOf()方法一共就只返回了两个不同的对象。假如某个线程的两个synchronized块之间发生了一次线程切换,那就会出现线程A等着被线程B持有的Integer.valueOf(1),而线程B又等着被线程A持有的Integer.valueOf(2),结果就出现了死锁等待。

线程死锁.png
线程死锁快照.png

3.2.多合一故障处理工具:VisualVM

3.3.内存分析工具:MAT(Eclipse Memory Analyzer Tool)

你可能感兴趣的:((七)虚拟机性能监控与故障处理工具)