JDK为我们提供了不少的工具,如下图所示

这些可执行文件都是不错的工具,掌握他们就是掌握JVM分析的武器库。
在windows平台下,他们是exe文件,在其他平台,文件格式会有所不同。
在linux中,一般默认会自带OpenJdk,一般情况下很多命令行工具不能用,要选择去安装相关插件,或者把OpenJdk卸载,然后重新安装Oracle的JDK。

1.命令行工具

1.1 jps

列出当前机器上正在运行的虚拟机进程(PS: JPS从操作系统的临时目录上去找,所以有一些信息可能显示不全)

  • -q: 仅仅显示进程
  • -m: 输出主函数传入的参数
  • -l:输出应用程序主类完整package名称或jar完整名称
  • -v:列出jvm参数(可以看到所有的vm options信息)

1.2 jstat

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

import java.util.LinkedList;
import java.util.List;

/**
 * @author 公众号:IT三明治
 * @date 2022/1/27
 */
public class StopWorld {

    public static class FillListThread extends Thread {
        List<byte[]> list = new LinkedList<>();

        @Override
        public void run() {
            while (true) {
                try {
                    if (list.size() * 512 / 1024 / 1024 >= 1000) {
                        System.out.println("Going to clear list, list size: " + list.size());
                        list.clear();
                        System.out.println("List is clean, list size: " + list.size());
                    }
                    byte[] bytes;
                    for (int i = 0; i < 100; i++) {
                        bytes = new byte[1024];
                        list.add(bytes);
                    }
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    public static class TimerThread extends Thread {
        public final static long startTime = System.currentTimeMillis();

        @Override
        public void run() {
            while (true) {
                try {
                    long t = System.currentTimeMillis() - startTime;
//                    System.out.println("程序执行了: "+t / 1000 + "." + t % 1000 + "秒");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        //制造GC,造成STW
        FillListThread fillListThread = new FillListThread();
        //时间打印线程
        TimerThread timerThread = new TimerThread();
        //两个线程同时启动
        fillListThread.start();
        timerThread.start();
    }
}

常用参数:

  • -class: 类加载器
    这里用到了jps查出来的进程id
  • -compiler(JIT Compiler即时编译器)
  • -gc(GC堆状态)

    S0C: 第一个幸存区(From区)的大小
    S1C: 第二个幸存区(To区)的大小
    S0U: 第一个幸存区的使用大小
    S1U: 第二个幸存区的使用大小
    EC: Eden区的大小
    S1U: Eden区的使用大小(Eden区用满就会马上清0)
    OC: 老年代大小
    OU: 老年代使用大小
    MC: 方法区大小
    MU: 方法区使用大小
    CCSC: 压缩类空间大小
    CCSU: 压缩类空间使用大小
    YGC: 年轻代垃圾回收次数
    YGC: 年轻代垃圾回收消耗时间
    FGC: 老年代垃圾回收次数
    FGCT: 老年代垃圾回收消耗时间
    GCT: 垃圾回收消耗总时间
    至于CGC和CGCT我在网络上并没有查到它们的准确解释,查了官方文档jdk11和目前最新的jdk17都没有这两个值的解释,这里查出来的结果也是无值,估计已经废弃了
    有兴趣的朋友请自己去查证
    https://docs.oracle.com/en/java/javase/11/tools/jstat.html
    https://docs.oracle.com/en/java/javase/17/docs/specs/man/jstat.html
  • -gccapacity(Memory pool generation and space capacities.) PS:老实说我还是觉得英文比较精准,同志们一定要养成看英文文档的习惯

    NGCMN: Minimum new generation capacity (KB).
    NGCMX: Maximum new generation capacity (KB).
    NGC: Current new generation capacity (KB).
    S0C: Current survivor space 0 capacity (KB).
    S1C: Current survivor space 1 capacity (KB).
    EC: Current eden space capacity (KB).
    OGCMN: Minimum old generation capacity (KB).
    OGCMX: Maximum old generation capacity (KB).
    OGC: Current old generation capacity (KB).
    OC: Current old space capacity (KB).
    MCMN: Minimum metaspace capacity (KB).
    MCMX: Maximum metaspace capacity (KB).
    MC: Metaspace Committed Size (KB).
    CCSMN: Compressed class space minimum capacity (KB).
    CCSMX: Compressed class space maximum capacity (KB).
    CCSC: Compressed class committed size (KB).
    YGC: Number of young generation GC events.
    FGC: Number of full GC events.
  • -gccause(最近一次GC统计和原因)

    This option displays the same summary of garbage collection statistics as the -gcutil option, but includes the causes of the last garbage collection event and (when applicable) the current garbage collection event. In addition to the columns listed for -gcutil, this option adds the following columns.
    LGCC: Cause of last garbage collection
    GCC: Cause of current garbage collection
  • -gcnew(新区统计)

    New generation statistics.
    S0C: Current survivor space 0 capacity (kB).
    S1C: Current survivor space 1 capacity (kB).
    S0U: Survivor space 0 utilization (kB).
    S1U: Survivor space 1 utilization (kB).
    TT: Tenuring threshold.
    MTT: Maximum tenuring threshold.
    DSS: Desired survivor size (kB).
    EC: Current eden space capacity (kB).
    EU: Eden space utilization (kB).
    YGC: Number of young generation GC events.
    YGCT: Young generation garbage collection time.
  • -gcnewcapacity(新区大小)

    New generation space size statistics.
    NGCMN: Minimum new generation capacity (KB).
    NGCMX: Maximum new generation capacity (KB).
    NGC: Current new generation capacity (KB).
    S0CMX: Maximum survivor space 0 capacity (KB).
    S0C: Current survivor space 0 capacity (KB).
    S1CMX: Maximum survivor space 1 capacity (KB).
    S1C: Current survivor space 1 capacity (KB).
    ECMX: Maximum eden space capacity (KB).
    EC: Current eden space capacity (KB).
    YGC: Number of young generation GC events.
    FGC: Number of full GC events.
  • -gcold(老区统计)

    Old generation size statistics.
    MC: Metaspace Committed Size (KB).
    MU: Metaspace utilization (KB).
    CCSC: Compressed class committed size (KB).
    CCSU: Compressed class space used (KB).
    OC: Current old space capacity (KB).
    OU: Old space utilization (KB).
    YGC: Number of young generation GC events.
    FGC: Number of full GC events.
    FGCT: Full garbage collection time.
    GCT: Total garbage collection time.
  • -gcoldcapacity(老区大小)

    Old generation statistics.
    OGCMN: Minimum old generation capacity (KB).
    OGCMX: Maximum old generation capacity (KB).
    OGC: Current old generation capacity (KB).
    OC: Current old space capacity (KB).
    YGC: Number of young generation GC events.
    FGC: Number of full GC events.
    FGCT: Full garbage collection time.
    GCT: Total garbage collection time.
  • -gcmetacapacity(元空间大小)

    Metaspace size statistics.
    MCMN: Minimum metaspace capacity (KB).
    MCMX: Maximum metaspace capacity (KB).
    MC: Metaspace Committed Size (KB).
    CCSMN: Compressed class space minimum capacity (KB).
    CCSMX: Compressed class space maximum capacity (KB).
    YGC: Number of young generation GC events.
    FGC: Number of full GC events.
    FGCT: Full garbage collection time.
    GCT: Total garbage collection time.
  • -gcutil(GC统计汇总)

    Summary of garbage collection statistics.
    S0: Survivor space 0 utilization as a percentage of the space’s current capacity.
    S1: Survivor space 1 utilization as a percentage of the space’s current capacity.
    E: Eden space utilization as a percentage of the space’s current capacity.
    O: Old space utilization as a percentage of the space’s current capacity.
    M: Metaspace utilization as a percentage of the space’s current capacity.
    CCS: Compressed class space utilization as a percentage.
    YGC: Number of young generation GC events.
    YGCT: Young generation garbage collection time.
    FGC: Number of full GC events.
    FGCT: Full garbage collection time.
    GCT: Total garbage collection time.
  • -printcompilation(HotSpot编译统计)

    Java HotSpot VM compiler method statistics.
    Compiled: Number of compilation tasks performed by the most recently compiled method.
    Size: Number of bytes of byte code of the most recently compiled method.
    Type: Compilation type of the most recently compiled method.
    Method: Class name and method name identifying the most recently compiled method. Class name uses a slash (/) instead of a dot (.) as a name space separator. The method name is the method within the specified class. The format for these two fields is consistent with the HotSpot -XX:+PrintCompilation option

我们要统计GC,就是垃圾回收,只需要以下命令

//43432是jvm进程,通过jsp命令得到,这样统计出来的是实时值  
jstat -gc 43432  

为了看到变化的值,可以用以下的方法

//每1000ms查询一次,一共查10次  
jstat -gc 43432 1000 10  

1.3 jinfo

查看和修改虚拟机参数

  • -sysprops: 查看可以由System.getProperties()获取到的参数

    以上参数太多,已经省略很多
  • -flag: 未被显式指定的参数的系统默认值(这个我后面详细介绍)
  • -flags: 比上面的多了(s),显示虚拟机的参数

下面用jinfo -flag来检查生产环境的gc情况
一般我们生产环境默认并不会打开gc日志,那运行时怎么查看gc日志呢?
jinfo -flag是可以修改jvm运行时的参数的。
我们先来看看哪些参数是可以修改的
执行以下命令可以看到Global flags非常多(-XX是指jvm的高级配置)

java -XX:+PrintFlagsFinal -version


因为参数实在太多,得向下翻页才能看到manageble的参数

manageble的参数就是支持运行时变更的。
我们来试试PrintGC,服务启动时我们没有打开打印GC日志,运行时怎么开启呢?
可以先用命令确认一下它的状态

PrintGC前面有一个"-"号表明它是false状态
我们要做的就是让它变成true状态,服务就可以开始打印GC日志了

再检查一下我们的日志发现真的多了GC日志了

等我们查到root cause,我们还要关闭生产服务器的GC日志
如下,第一行是关闭,第二行是确认已经关闭

1.4 jmap

用于生成转储快照(又叫heapdump或者dump文件)。
jmap的作用不仅仅是为了获取dump文件,它还可以查询finalize执行队列、Java堆和永久代的详细信息,如空间使用率、当前用的是哪种垃圾收集器等。
和jinfo一样, jmap有不少功能在Windows平台都是受限制的,除了生成dump文件的-dump选项和用于查看每个类型的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linxu\Solaris下使用。

jmap -heap 打印heap的概要信息

我试了一下就报错了,原来java 8以后的版本命令改了
改成新命令
jhsdb jmap --heap --pid 26688
还是出错

原来我java用的是jdk 11

但是我执行程序用的是java 8

切换到java 11执行

然后再执行命令

这里可以看到jmap打印的heap的快照信息都是用全称,一目了然(不像jstat都是用简称)

jmap -histo 打印历史信息

这些信息我们可以看到哪些对象实例数量最多,哪些对象占用内存空间最大,分析它们可以用于jvm调优。
jmap -histo:live 只打印存活对象的历史信息

jmap -histo | head -20 只显示前20条信息(不过这个命令在windows不可行)

jmap -finalizerinfo 打印正在等候回收对象的信息
如果没有需要等待回收的信息就会出现下面的信息

jmap -dump 堆转储快照
这个命令会把占用内存的数据原封不动转存出来,非常耗性能,而且非常浪费磁盘空间,还非常占用io,不适合在生产环境的高峰期用

这样转存文件就生成了,还不小,这个测试程序已经占1个多G了

1.5 jhat

Java Head Analyse Tool, 这个工具可以分析堆转储文件
这是一个实验性的工具,还挺强大的,它从jdk6就开始支持,但是在jdk9和jdk10就已经不支持了,我特地去官网求证了
https://docs.oracle.com/en/java/javase/17/migrate/removed-tools-and-components.html#GUID-B49A964D-A2EF-4DAF-8A71-A64EF3E77C00

我这里配置的是jdk 11所以我这里的java环境是测试不了的

但是…
程序员是爱折腾的,且等我把jdk装回版本8我们再一起玩转它(ps: 修改完环境变量记得重启cmd命令行窗口,不然jdk版本不会更新)


Ok,现在可以玩了。
在1.4我转储了一个dump快照,我们现在把它理解为是一个生产环境的快照,现在我们要分析它。
有时你dump出来的堆很大,在启动时会报堆空间不足的错误,可以使用如下参数:

jhat -J-Xmx512m


看到了吗? 读完Dump文件之后,它还用默认端口7000起了一个server。
现在我们要访问这个服务

接下来我们就可以看看它有什么好东西了

  • 1.显示出堆中所包含的所有的类和主程序中的类

  • 2.从根集能引用到的对象
  • 3.显示包括平台的所有类的实例数量
  • 4.显示不包含平台的所有类的实例数量(这才是我的main方法里面用到的实例)
  • 5.堆实例的分布表

    这个我们前面用jmap -histo 也包含了这部分信息
  • 6.等待回收的对象信息
  • 7.执行对象查询语句

正因为转储文件过大,分析转储文件也很占用资源,生产环境一般不推荐这么用

我现在命令行工具执行这个命令占用内存都超过了400M了
我退出这个命令再看看它的正常内存占用

平常它占用内存也就几个M
这也是为什么jhat在java9后被淘汰了,我们现在有了更强大的可视化工具(我后面会介绍),其实也不需要用到它了。

1.6 jstack

(Stack Trace for Java) 该命令用于生成虚拟机当前时刻的线程快照。
线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程死锁、死循环、请求外部资源导致的长时间等待都是导致线程长时间停顿的常见原因。
在代码中可以用 java.lang.Thread 类的 getAllStackTraces()方法用于获取虚拟机中所有线程的StackTraceElement对象。使用这个方法可以通过简单的几行代码就完成 jstack的大部分功能,在实际项目中不妨调用这个方法做个管理员页面,可以随时使用浏览器来查看线程堆栈。
一般来说 jstack 主要是用来排查是否有死锁的情况。
先来看看实例

/**
 * @author 公众号:IT三明治
 * @date 2022/1/27
 */
public class DeadLock {

    /**
     * 第一个对象锁
     */
    private static Object closeTheDoor = new Object();
    /**
     * 第二个对象锁
     */
    private static Object openTheDoor = new Object();

    private static void sandwichDo() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (openTheDoor) {
            System.out.println(threadName + " open the door");
            Thread.sleep(1000);
            synchronized (closeTheDoor) {
                System.out.println(threadName + " close the door");
            }
        }
    }

    private static void michaelDo() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (closeTheDoor) {
            System.out.println(threadName + " close the door");
            Thread.sleep(1000);
            synchronized (openTheDoor) {
                System.out.println(threadName + " open the door");
            }
        }
    }

    private static class SandwichThread extends Thread {
        private String name;
        public SandwichThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                sandwichDo();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static class MichaelThread extends Thread {
        private String name;
        public MichaelThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                michaelDo();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SandwichThread sandwichThread = new SandwichThread("Sandwich");
        MichaelThread michaelThread = new MichaelThread("Michael");
        sandwichThread.start();
        michaelThread.start();
    }
}

程序跑起来然后看看线程快照

两个线程都已经blocked住了
再往下看快照信息

发现线程死锁了。

2.可视化工具

可视化工具其实是汇总了命令行工具的功能结合UI显示出来。
说到可视化工具,不得不说JMX.
JMX(Java Management Extensions,即Java管理扩展)是一个为应用程序、设备、系统等植入管理功能的框架。JMX可以跨越一系列异构操作系统平台、 系统体系结构和网络传输协议,灵活的开发无缝集成的系统、网络和服务管理应用。
管理远程进程需要在远程程序的启动参数中增加:

-Djava.rmi.server.hostname=……
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=10000
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

2.1 Jconsole

在jdk的bin路径下找这个工具

可以访问本地进程或者远程进程, 这里我们直接用本地进程测试

进入进程就可以看到以下概览界面

这个概览页面可以看到时间线上的堆内存使用量,线程数,类加载数和cpu占用率,后面的tab有更多细节可以查看

2.1.1 内存图表


如果发现内存占用过高,可以点“执行GC”,强制触发一次垃圾回收(类似于java代码中执行一次System.gc())

2.1.2 线程图表

2.1.3 类图表


这个图表可以看类加载和卸载情况,如果有运行时类加载卸载可能更直观

2.1.4 VM概要


我添加一点vm options再重启程序

重新连接Jconsole(因为重启进程id会变的,需要重连)

可以发现vm options变化了
用jinfo查到的结果一样

我用jinfo把运行时的PrintGC关掉

这里的vm options并没有改变(重连也是一样)

可见jconsole这里监控的只是启动时的vm options

  • MBean

    因为我只是启动了一个Main方法,在里面启动两个线程,所以在这里看到的MBean并不多。
    真正生产的服务进程MBean是非常多的,在这里查可以看MBean的属性,集合对象的size,以及对象的json值,非常实用。

2.2 jvisualvm

VisualVM 提供了一个可视界面,用于查看 Java 虚拟机 (Java Virtual Machine, JVM) 上运行的基于 Java 技术的应用程序的详细信息。VisualVM对Java Development Kit (JDK) 工具所检索的JVM软件相关数据进行组织,并通过一种使您可以快速查看有关多个 Java 应用程序的数据的方式提供该信息。您可以查看本地应用程序或远程主机上运行的应用程序的相关数据。此外,还可以捕获有关JVM 软件实例的数据,并将该数据保存到本地系统,以供后期查看或与其他用户共享。
打开visualvm

它跟jconsole一样也分本地和远程访问

我们这里只看本地,双击一个本地进程就可以打开概述页面

2.2.1 监视


这里同jconsole一样可以强制执行垃圾回收,也可以执行堆Dump转存。
下图是我转存的一个head dump文件(ps: 转存文件好大,900多兆了)

生产的head dump文件会挂载在这里,双击就可以打开,可以对比不同时间的head dump, 有利于分析不同时间的堆转储信息

2.2.2 线程


转存的线程Dump文件也挂载在当前进程下

从这个线程快照可以看到我写的两个线程“Thread-0”和“Thread-1”正在睡觉

2.2.3 抽样器


可以看到类实例按照内存大小和实例数倒序排列
这里看到的信息跟jmap -histo看到的内容是一样的,只是可视化工具更直观

2.2.4 Profiler

CPU Profiling:CPU Profiling的主要目的是统计函数的调用情况及执行时间,或者更简单的情况就是统计应用程序的CPU使用情况。
内存Profiling:内存 Profiling的主要目的是通过统计内存使用情况检测可能存在的内存泄露问题及确定优化内存使用的方向。

2.2.5 插件

jvisualvm还有一个亮点就是他支持插件
我们现在安装一个Visual GC来玩玩

安装完插件需要把当前进程关掉重新打开才能看到你的新插件

把Refresh rate调到最快的100毫秒,接下来看Visual GC表演了

这里可以非常直观地看到Eden区的使用都是用满然后全部转到Survier0,它自己的内存马上清空了,然后Survier0转存到Survier1的时候,老年代会同时给它增加了一份备份了。垃圾最后在老年代被回收。
元空间的数据非常稳定,一般运行时很少做回收。
ps: 不同的jdk所带的visualvm是不同的,下载插件时会根据你的jdk下载不同的版本。
还有不少好用的插件呢,有兴趣的朋友自己下载研究吧。

武器虽多,也要自己收藏好,才能为你所用。
请关注我,我下期给你带来更强大的武器

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐