本文结合深入理解 Java 虚拟机对 JDK 中涉及的若干性能调优及监测工具进行简单使用。

测试代码

首先给出用于测试的 .java 文件
主函数里启动两个线程,每个线程都执行死循环。循环内部对一个 Point 对象的坐标进行随机更新,当循环指定次数后向 List 中追加新的对象,使得堆空间不断增长。

Main.java

public class Main {
    public static void main(String[] args) throws RuntimeException {
        Thread t1 = new Thread(new MyThread());
        t1.start();
        Thread t2 = new Thread(new MyThread2());
        t2.start();
    }
}

MyThread.java

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/*
    一个占用空间很大的类,模拟实际业务
 */
class BigClass{
    List<Double> list;
    BigClass() {
        list = new ArrayList<>();
        for (int i=0; i<100000; i++) {
            list.add(i*1.0);
        }
    }
}
class MyThread implements Runnable{
    @Override
    public void run() {
        Point p = new Point(0, 0, "Thread-1");
        Random rt = new Random();
        int cnt = 0;
        while (true) {
            int dx = rt.nextInt(10);
            int dy = rt.nextInt(10);
            if ((dx&1)==1) {
                p.add(dx, dy);
            }
            else {
                p.plus(dx, dy);
            }
            if (cnt%3==0) {
                Main.l1.add(new BigClass());
                System.out.println("Generated by 1");
            }
            p.print();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cnt++;
        }
    }
}
class MyThread2 implements Runnable{
    @Override
    public void run() {
        Point p = new Point(0, 0, "Thread-2");
        Random rt = new Random();
        int cnt = 0;
        while (true) {
            int dx = rt.nextInt(10);
            int dy = rt.nextInt(10);
            if ((dx&1)==1) {
                p.add(dx, dy);
            }
            else {
                p.plus(dx, dy);
            }
            if (cnt%3==0) {
                Main.l2.add(new BigClass());
                System.out.println("Generated by 2");
            }
            p.print();
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cnt++;
        }
    }
}

Point.java

class Point {
    private int x;
    private int y;
    private String name;
    Point(int x, int y, String name) {
        this.x = x;
        this.y = y;
        this.name = name;
    }
    public void add(int dx, int dy) {
        x += dx;
        y += dy;
    }
    public void plus(int dx, int dy) {
        x -= dx;
        y -= dy;
    }
    public void print() {
        System.out.println(x+ "  "+y+"  "+"   "+name);
    }
}

jps

jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息。

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

运行 Main 类后在命令行中输入如下命令并观看执行结果:

图片说明

图片说明

jstat

jstat 是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有 GUI 图形界面只提供纯文本控制台环境的服务器上,它是运行期定位虚拟机性能问题的首选工具。jstat 主要工具选项如下:

  • -class:监视类装载、卸载数量、总空间以及类装载所耗费的时间
  • -gc:监视 java 堆状态,包括 Eden, Survivor, 老年代, 永久代的容量、已用空间、GC 时间等信息
  • -gccapacity:同 -gc ,但是更关注堆空间使用的最大最小空间
  • -gcutil:同 -gc ,但是更关注已使用空间占总空间的百分比
  • -gccause:同 -gcutil ,但是更关注上一次 GC 产生的原因
  • -gcnew:监视新生代 GC 情况
  • -gcnewcapacity:同 -gcnew ,但是更关注使用到的最大、最小空间
  • -gcold:监视老年代 GC 情况
  • -gcoldcapacity:同 -gcold,但是更关注使用到的最大、最小空间
  • -gcpermcapacity:监视永久代使用的最大、最小空间

运行 Main 类后在命令行中输入如下命令(jstat -参数 pid 时间间隔 采样次数)并观看执行结果:

图片说明

图片说明

图片说明

信息统计中各列含义说明如下:

  • S0C:年轻代中第一个survivor(幸存区)的容量 (字节)
  • S1C:年轻代中第二个survivor(幸存区)的容量 (字节)
  • S0U:年轻代中第一个survivor(幸存区)目前已使用空间 (字节)
  • S1U:年轻代中第二个survivor(幸存区)目前已使用空间 (字节)
  • EC:年轻代中Eden(伊甸园)的容量 (字节)
  • EU:年轻代中Eden(伊甸园)目前已使用空间 (字节)
  • OC:Old代的容量 (字节)
  • OU:Old代目前已使用空间 (字节)
  • PC:Perm(持久代)的容量 (字节)
  • PU:Perm(持久代)目前已使用空间 (字节)
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • YGC:从应用程序启动到采样时年轻代中gc次数
  • YGCT:从应用程序启动到采样时年轻代中gc所用时间(s)
  • FGC:从应用程序启动到采样时old代(全gc)gc次数
  • FGCT:从应用程序启动到采样时old代(全gc)gc所用时间(s)
  • GCT:从应用程序启动到采样时gc用的总时间(s)
  • NGCMN:年轻代(young)中初始化(最小)的大小 (字节)
  • NGCMX:年轻代(young)的最大容量 (字节)
  • NGC:年轻代(young)中当前的容量 (字节)
  • OGCMN:old代中初始化(最小)的大小 (字节)
  • OGCMX:old代的最大容量 (字节)
  • OGC:old代当前新生成的容量 (字节)
  • PGCMN:perm代中初始化(最小)的大小 (字节)
  • PGCMX:perm代的最大容量 (字节)
  • PGC:perm代当前新生成的容量 (字节)
  • S0:年轻代中第一个survivor(幸存区)已使用的占当前容量百分比
  • S1:年轻代中第二个survivor(幸存区)已使用的占当前容量百分比
  • E:年轻代中Eden(伊甸园)已使用的占当前容量百分比
  • O:old代已使用的占当前容量百分比
  • P:perm代已使用的占当前容量百分比
  • S0CMX:年轻代中第一个survivor(幸存区)的最大容量 (字节)
  • S1CMX :年轻代中第二个survivor(幸存区)的最大容量 (字节)
  • ECMX:年轻代中Eden(伊甸园)的最大容量 (字节)
  • DSS:当前需要survivor(幸存区)的容量 (字节)(Eden区已满)
  • TT: 持有次数限制
  • MTT : 最大持有次数限制

jinfo

jinfo 的作用是实时查看调整虚拟机各项参数。当系统崩溃时,jinfo 可以从 core 文件里面知道崩溃的 Java 应用程序的配置信息。选项说明如下:

  • no option 输出全部的参数和系统属性
  • -flag name 输出对应名称的参数
  • -flag [+|-]name 开启或者关闭对应名称的参数
  • -flag name=value 设定对应名称的参数
  • -flags 输出全部的参数
  • -sysprops 输出系统属性

运行 Main 类后在命令行中输入如下命令并观看执行结果:

图片说明

图片说明

图片说明

图片说明

图片说明

jmap

jmap 命令用于生成堆存储快照,还可以查询 finalize 执行队列、 Java 对和永久代的详细信息,如空间使用率、当前用的是哪种收集器等。主要选项如下:

  • -dump:生成 Java 堆存储快照
  • -finalizerinfo:显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象
  • -heap:显示 Java 堆详细信息,比如使用哪种回收器、参数配置、分代状况等
  • -histo:显示堆中对象统计信息,包括类、实例数量、合计数量
  • -permstat:显示永久代内存状态
  • -F:强制选项

运行 Main 类后在命令行中输入如下命令并观看执行结果:

图片说明

图片说明

图片说明

图片说明

jhat

jhat 是 JDK 内置的工具之一。主要是用来分析 Java 堆的命令,可以将堆中的对象以 Html 的形式显示出来,包括对象的数量,大小等等,并支持对象查询语言。运行 Main 类后在命令行中输入如下命令并观看执行结果:

图片说明

随后在浏览器中访问 http://localhost:7000/ 即可。

jstack

jstack 命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈集合,生成线程快照的主要目的在于定位线程长时间停顿的原因,比如线程死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的原因。主要选项如下:

  • -F:强制
  • -l:除堆栈外,显示关于锁的附加信息
  • -m:如果调用本地方法,可以显示C/C++的堆栈

运行 Main 类后在命令行中输入如下命令并观看执行结果:

图片说明

下面给出一个检测死锁的例子,代码如下:

public class DeadLock {
    private static Lock lock1 = new ReentrantLock();
    private static Lock lock2 = new ReentrantLock();
    public static void deathLock() {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                try {
                    lock1.lock();
                    TimeUnit.SECONDS.sleep(1);
                    lock2.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    lock2.lock();
                    TimeUnit.SECONDS.sleep(1);
                    lock1.lock();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        t1.setName("mythread1");
        t2.setName("mythread2");
        t1.start();
        t2.start();
    }
    public static void main(String[] args) {
        deathLock();
    }

运行这个类后,我们在命令行中观看其输出如下:

图片说明

首先,使用 -l 参数输出了关于死锁的信息。接下来定位到两个线程的信息输出位置:

图片说明