天天看点

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

作者:一个即将被退役的码农

注意: 篇幅较长,建议收藏后再仔细阅读!!!!!!!!!!

目录:

一.引言

二.基础故障处理工具

2.1 概述

2.2. jps:虚拟机进程状况工具

2.3. jstat:虚拟机统计信息监视工具

2.3. jinfo:Java配置信息工具

2.5. jmap:Java内存映像工具

2.7. jstack:Java堆栈跟踪工具

2.8. 基础工具总结

三. 可视化故障处理工具

3.1. JHSDB:基于服务性代理的调试工具

3.2. JConsole:Java监视与管理控制台

3.3. JVisualVM:多合一故障处理工具

3.4. Java Mission Control(JMC):可持续在线的监控工具

四. HotSpot虚拟机插件及工具

一.引言

给一个系统定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。

异常堆栈、虚拟机运行日志、垃圾收集器日志、线程快照(threaddump/javacore文件)、堆转储快照(heapdump/hprof文件)。

工具永远都是知识技能的一层包装,没有什么工具是“秘密武器”,拥有了就能“包治百病”。

二.基础故障处理工具

2.1 概述

选择采用Java语言本身来实现这些故障处理工具

当应用程序部署到生产环境后,无论是人工物理接触到服务器还是远程Telnet到服务器上都可能会受到限制。

借助这些工具类库里面的接口和实现代码,开发者可以选择直接在应用程序中提供功能强大的监控分析功能。

启用 JMX 功能

JDK5或以下版本,在程序启动时请添加参数“-Dcom.sun.management.jmxremote”开启JMX管理功能。

JDK6或以上版本,默认开启了JMX管理。

2.2. jps:虚拟机进程状况工具

JDK的很多小工具的名字都参考了UNIX命令的命名方式,jps(JVM Process Status Tool)是其中的典型。除了名字像UNIX的ps命令之外,它的功能也和ps命令类似:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main()函数所在的类)名称以及这些进程的本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)。虽然功能比较单一,但它绝对是使用频率最高的JDK 命令行工具,因为其他的JDK工具大多需要输入它查询到的LVMID来确定要监控的是哪一个虚拟机进程。对于本地虚拟机进程来说,LVMID与操作系统的进程ID(PID,Process Identifier)是一致的,使用Windows的任务管理器或者UNIX的ps命令也可以查询到虚拟机进程的LVMID,但如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就必须依赖jps命令显示主类的功能才能区分了。

jps命令格式:

jps [ options ] [ hostid ]

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

jps常用的option选项:

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

案例

public class Jstat {
    /**
     * vm参数为 -Xms30m -Xmx30m -Xmn10m
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(1000000);
    }
}           

运行之后,使用jps命令,将会展示虚拟机进程id和名字:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

2.3. jinfo:Java配置信息工具

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

jstat的命令格式:

jstat [option vmid [interval [s|ms] [count]] ]

1. option: 参数选项

2. -t: 可以在打印的列加上Timestamp列,用于显示系统运行的时间

3. -h: 可以在周期性数据数据的时候,可以在指定输出多少行以后输出一次表头

4. vmid: Virtual Machine ID( 进程的 pid)

5. interval: 执行每次的间隔时间,单位为毫秒

6. count: 用于指定输出多少次记录,缺省则会一直打印

7. 对于命令格式中的VMID和LVMID,如过是本地虚拟机进程,VMID和LVMID是一致的,如 果是远程虚拟机,那VMID的格式应当是:[protocol:] [//] lvmid[@hostname[:port]/servername]

8. 参数interval 和count分别表示查询的间隔和次数,如果省略这两个参数,说明只查询一次。

Jstat常用option选项:

选项 作用
-class 类装载数量、卸载数量、总空间以及类状态所消耗时间
-GC 监视Java堆容量状况,包括Eden、Survivor、老年代、永久代等
-GCcapacity 监视Java堆最大、最小空间
-GCutil 关注已使用空间占总空间的百分比
-GCcause 类似GCutil,额外输出上次GC的原因
-GCnew 新生代GC状况
-GCnewcapacity 与-GCnew类似,输出主要关注使用到的最大、最小空间
-GCold 老年代GC状况
-GColdcapacity 与-GCold类似,输出主要关注使用到的最大、最小空间
-GCpermcapacity 输出永久代使用到的最大、最小空间
-compiler 输出JIT编译过的方法和耗时
-printcompilation 输出已经被JIT编译的方法
-GCmetacapacity 元数据空间统计

案例

加上-GC显示将会GC堆信息,使用上一个案例,设置VM参数为 -Xms30m -Xmx30m -Xmn10m ,即初始内存30m,最大内存30m,年轻代10m。

运行程序,使用 jstat -gc 6128 命令结果如下,可以看到GC堆信息:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

S0C:年轻代中第一个Survivor(幸存区)的容量 (字节)

S1C:年轻代中第二个Survivor(幸存区)的容量 (字节)

S0U :年轻代中第一个Survivor(幸存区)目前已使用空间 (字节)

S1U :年轻代中第二个Survivor(幸存区)目前已使用空间 (字节)

EC :年轻代中Eden(伊甸园)的容量 (字节)

EU :年轻代中Eden(伊甸园)目前已使用空间 (字节)

OC :Old代的容量 (字节)

OU :Old代目前已使用空间 (字节)

MC:metaspace(元空间)的容量 (字节)

MU:metaspace(元空间)目前已使用空间 (字节)

YGC :从应用程序启动到采样时年轻代中GC次数

YGCT :从应用程序启动到采样时年轻代中GC所用时间(s)

FGC :从应用程序启动到采样时old代(全GC)GC次数

FGCT :从应用程序启动到采样时old代(全GC)GC所用时间(s)

GCT:从应用程序启动到采样时GC用的总时间(s)

从图中可以看出,各项结果符合我们的VM参数设置的信息。

2.5. jmap:Java内存映像工具

jmap命令用于生成堆转储快照。jmap的作用并不仅仅为了获取dump文件,它还可以查询finalize执行队列、java堆和永久代的详细信息。如空间使用率、当前用的是哪种收集器等。

jmap格式:

jmap [option] vmid

jmap常用option选项:

选项 作用
-dump 生成堆转储快照,格式为-dump:[live,]format=b,file=,不建议使用
-finalizerinfo 显示在F-Queue中等待Finalizer线程执行finalize方法的对象
-heap 显示java堆详细信息,回收器种类、参数配置、分代状况等
-histo 显示堆中对象统计信息,包括类、实例数量、合计容量,会先触发GC,再统计信息,不建议使用
-permstat 查看永久代内存状态,比较耗时,会暂停应用,不建议使用

案例:

还是上面的例子。

使用jmap -heap 6128,可以看到我们的VM参数设置的信息:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

生成dump文件

jmap -dump:live,format=b,file=‪C:\Users\lx\Desktop\test1.bin 9472

将生成堆转储快照,这里我生成到桌面。后面可以使用jhat分析dump文件。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

2.7. jstack:Java堆栈跟踪工具

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

jstack 格式:

jstack [option] vmid

jstack常见option选项:

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

案例

jstack -l 9472

会输出很多信息,我们可以找到如下信息:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

可以看到,main线程正在限时等待——因为sleep的原因。

jstack 可以帮助我们用来分析线程信息,比如死锁,状态等。

2.8. 基础工具总结

  1. jps将打印所有正在运行的 Java 进程。
  2. jstat允许用户查看目标 Java 进程的类加载、即时编译以及垃圾回收相关的信息。它常用于检测垃圾回收问题以及内存泄漏问题。
  3. jmap允许用户统计目标 Java 进程的堆中存放的 Java 对象,并将它们导出成二进制文件。
  4. jinfo将打印目标 Java 进程的配置参数,并能够改动其中 manageabe 的参数。
  5. jstack将打印目标 Java 进程中各个线程的栈轨迹、线程状态、锁状况等信息。它还将自动检测死锁。

三. 可视化故障处理工具

3.1. JHSDB:基于服务性代理的调试工具

HSDB(Hotspot Debugger),是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除。

3.1.1 HSDB发展

sa-jdi.jar

在 Java9 之前,JAVA_HOME/lib 目录下有个 sa-jdi.jar,可以通过如下命令启动HSDB(图形界面)及CLHSDB(命令行)。

java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
           

sa-jdi.jar中的sa的全称为 Serviceability Agent,它之前是sun公司提供的一个用于协助调试 HotSpot 的组件,而 HSDB 便是使用Serviceability Agent 来实现的。

由于Serviceability Agent 在使用的时候会先attach进程,然后暂停进程进行snapshot,最后deattach进程(进程恢复运行),所以在使用 HSDB 时要注意。

jhsdb

jhsdb 是 Java9 引入的,可以在 JAVA_HOME/bin 目录下找到 jhsdb;它取代了 JDK9 之前的 JAVA_HOME/lib/sa-jdi.jar,可以通过下述命令来启动 HSDB。

$ cd /Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home/bin/
$ jhsdb hsdb
           

jhsdb 有 clhsdb、debugd、hsdb、jstack、jmap、jinfo、jsnap 这些 mode 可以使用。

其中 hsdb 为 ui debugger,就是 jdk9 之前的 sun.jvm.hotspot.HSDB;而 clhsdb 即为 jdk9 之前的sun.jvm.hotspot.CLHSDB。

3.1.2 HSDB实操

3.1.2.1 启动HSDB

检测不同 JDK 版本需要使用不同的 HSDB 版本,否则容易出现无法扫描到对象等莫名其妙的问题。

Mac:JDK7 和 JDK8 均可以采用以下的方式

$ java -cp /Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB
           

如果执行报错,则前面加上 sudo,或者更改 sa-jdi.jar 的权限。

sudo chmod -R 777 sa-jdi.jar 
           

本地安装的是 JDK8,在启动 HSDB 后,发现无法连接到 Java 进程,在 attach 过程中会提示如下错误:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

网上搜索相关解决方案,建议更换 JDK 版本。可以去参考 Mac下安装多个版本的JDK并随意切换

个人在配置的过程中遇到了这样一个问题:在切换 JDK 版本时,发现不生效,网上各种查找方案,动手尝试,最后都没有成功。解决方案:手动修改 .bash_profile 文件,增加注释。

首次尝试 JDK 11,但是还是无法 attach Java 进程,试了好久都不行,只能再次尝试 JDK9.

而 JDK9 的启动方式有些区别

$ cd /Library/Java/JavaVirtualMachines/jdk-9.0.4.jdk/Contents/Home/bin/
$ jhsdb hsdb
           

其中启动版本可以使用 /usr/libexec/java_home -V 获取 HSDB 对 Serial GC 支持的较好,因此 Debug 时增加参数 -XX:+UseSerialGC。注意运行程序 Java 的版本和 hsdb 的 Java 版本要一致才行。

注意:如果后续想要下载 .class 文件,启动 hsdb 时,需要执行 sudo jhsdb hsdb 命令。

3.1.2.2 HSDB可视化界面

比如说有这么一个 Java 程序,我们使用 Thread.sleep 方法让其长久等待,然后获取其进程 id。

public class InvokeTest {
      

  public static void printException(int num) {
      
    new Exception("#" + num).printStackTrace();
  }

  public static void main(String[] args)
      throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, InterruptedException {
      
    Class<?> cl = Class.forName("InvokeTest");
    Method method = cl.getMethod("printException", int.class);
    for (int i = 1; i < 20; i++) {
      
      method.invoke(null, i);
      if (i == 17) {
      
        Thread.sleep(Integer.MAX_VALUE);
      }
    }
  }
}
           

然后在 terminal 窗口执行 jps 命令:

27995 InvokeTest
           

然后在 HSDB 界面点击 file 的 attach,输入 pid,如果按照上述步骤操作,是可以操作成功的。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

attach 成功后,效果如下所示:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

更多操作选择推荐阅读:解读HSDB

3.1.2.3分析对象存储区域

下面代码中的 heatStatic、heat、heatWay 分别存储在什么地方呢?

package com.msdn.java.hotspot.hsdb;

public class Heat2 {
      

  private static Heat heatStatic = new Heat();
  private Heat heat = new Heat();

  public void generate() {
      
    Heat heatWay = new Heat();
    System.out.println("way way");
  }
}

class Heat{
      

}
           

测试类

package com.msdn.java.hotspot.hsdb;

public class HeatTest {
      
  public static void main(String[] args) {
      
    Heat2 heat2 = new Heat2();
    heat2.generate();
  }
}
           

关于上述问题,我们大概都知道该怎么回答:

heatStatic 属于静态变量,引用应该是放在方法区中,对象实例位于堆中;

heat 属于成员变量,在堆上,作为 Heat2 对象实例的属性字段;

heatWay 属于局部变量,位于 Java 线程的调用栈上。

那么如何来看看这些变量在 JVM 中是怎么存储的?这里借助 HSDB 工具来进行演示。

此处我们使用 IDEA 进行断点调试,后续会再介绍 JDB 如何进行代码调试。

IDEA 执行前需要增加 JVM 参数配置,HSDB 对 Serial GC 支持的较好,因此 Debug 时增加参数 -XX:+UseSerialGC;此外设置 Java Heap 为 10MB;UseCompressedOops 参数用来压缩 64位指针,节省内存空间。关于该参数的详细介绍,推荐阅读本文。

最终 JVM 参数配置如下:

-XX:+UseSerialGC  -Xmn10M -XX:-UseCompressedOops
           
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

然后在 Heat2 中的 System 语句处打上断点,开始 debug 执行上述代码。

接着打开命令行窗口执行 jps 命令查看我们要调试的 Java 进程的 pid 是多少:

% jps
9977 HeatTest
           

接着我们按照上文讲解启动 HSDB,注意在 IDEA 中执行代码时,Java 版本为 Java9,要与 HSDB 相关的 Java 版本一致。

在 attach 成功后,选中 main线程并打开其栈信息,接着打开 console 窗口,下面我将自测的命令及结果都列举了出来,并简要介绍其作用,以及可能遇到的问题。

首先执行 help 命令,查看所有可用的命令

hsdb> help
Available commands:
  assert true | false
  attach pid | exec core
  buildreplayjars [ all | app | boot ]  | [ prefix ]
  detach
  dis address [length]
  disassemble address
  dumpcfg {
       -a | id }
  dumpcodecache
  dumpideal {
       -a | id }
  dumpilt {
       -a | id }
  dumpreplaydata {
       <address > | -a | <thread_id> }
  echo [ true | false ]
  examine [ address/count ] | [ address,address]
  field [ type [ name fieldtype isStatic offset address ] ]
  findpc address
  flags [ flag | -nd ]
  help [ command ]
  history
  inspect expression
  intConstant [ name [ value ] ]
  jdis address
  jhisto
  jstack [-v]
  livenmethods
  longConstant [ name [ value ] ]
  pmap
  print expression
  printall
  printas type expression
  printmdo [ -a | expression ]
  printstatics [ type ]
  pstack [-v]
  quit
  reattach
  revptrs address
  scanoops start end [ type ]
  search [ heap | perm | rawheap | codecache | threads ] value
  source filename
  symbol address
  symboldump
  symboltable name
  thread {
       -a | id }
  threads
  tokenize ...
  type [ type [ name super isOop isInteger isUnsigned size ] ]
  universe
  verbose true | false
  versioncheck [ true | false ]
  vmstructsdump
  where {
       -a | id }
  
  hsdb> where 3587
Thread 3587 Address: 0x00007fb25c00a800

Java Stack Trace for main
Thread state = BLOCKED
 - public void generate() @0x0000000116953ff8 @bci = 8, line = 15, pc = 0x0000000123cdacd7, oop = 0x000000013316f128 (Interpreted)
 - public static void main(java.lang.String[]) @0x00000001169539b0 @bci = 9, line = 11, pc = 0x0000000123caf4ba (Interpreted)

hsdb> 
           

3.1.2.4 主要命令简介

命令1、universe 命令来查看GC堆的地址范围和使用情况,可以看到我们创建的三个对象都是在 eden 区。因为使用的是 Java9,所以已经不存在 Perm gen 区了,

hsdb> universe
Heap Parameters:
Gen 0:   eden [0x0000000132e00000,0x000000013318c970,0x0000000133600000) space capacity = 8388608, 44.36473846435547 used
  from [0x0000000133600000,0x0000000133600000,0x0000000133700000) space capacity = 1048576, 0.0 used
  to   [0x0000000133700000,0x0000000133700000,0x0000000133800000) space capacity = 1048576, 0.0 usedInvocations: 0

Gen 1:   old  [0x0000000133800000,0x0000000133800000,0x0000000142e00000) space capacity = 257949696, 0.0 usedInvocations: 0
           

不借助命令的话,还可以这样操作来查看。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

命令2、scanoops 查看类型

Java 代码里,执行到 System 输出语句时应该创建了3个 Heat 的实例,它们必然在 GC 堆里,但都在哪里,可以用scanoops命令来看:

hsdb> scanoops 0x0000000132e00000 0x000000013318c970 com.msdn.java.hotspot.hsdb.Heat
0x000000013316f118 com/msdn/java/hotspot/hsdb/Heat
0x000000013316f140 com/msdn/java/hotspot/hsdb/Heat
0x000000013316f150 com/msdn/java/hotspot/hsdb/Heat
           

scanoops 接受两个必选参数和一个可选参数:必选参数是要扫描的地址范围,一个是起始地址一个是结束地址;可选参数用于指定要扫描什么类型的对象实例。实际扫描的时候会扫出指定的类型及其派生类的实例。

从 universe 命令返回结果可知,对象是在 eden 里分配的内存(注意used),所以执行 scanoops 命令时地址范围可以从 eden 中获取。

命令3、findpc 命令可以进一步知道这些对象都在 eden 之中分配给 main 线程的

thread-local allocation buffer (TLAB)中

网上的多数文章都介绍 whatis 命令,不过我个人在尝试的过程中执行该命令报错,如下述所示:

hsdb> whatis 0x000000012736efe8
Unrecognized command.  Try help...
           

命令不行,那么换种思路,使用 HSDB 可视化窗口来查看对象的地址信息。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

至于为什么无法使用 whatis 命令,原因是 Java9 的 HSDB 已经没有 whatis 命令了,取而代之的是 findpc 命令。

hsdb> findpc 0x000000013316f118
Address 0x000000013316f118: In thread-local allocation buffer for thread "main" (3587)  [0x00000001331639f8,0x000000013316f160,0x000000013318c730,{
      0x000000013318c970})
           

命令4、inspect命令来查看对象的内容:

hsdb> inspect 0x000000013316f118
instance of Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f118 @ 0x000000013316f118 (size = 16)
_mark: 1
_metadata._klass: InstanceKlass for com/msdn/java/hotspot/hsdb/Heat
           

可见一个 heatStatic 实例要16字节。因为 Heat 类没有任何 Java 层的实例字段,这里就没有任何 Java 实例字段可显示。

或者通过可视化工具来查看:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

一个 Heat 的实例包含 2个给 VM 用的隐含字段作为对象头,和0个Java字段。

对象头的第一个字段是mark word,记录该对象的GC状态、同步状态、identity hash code之类的多种信息。

对象头的第二个字段是个类型信息指针,klass pointer。这里因为默认开启了压缩指针,所以本来应该是64位的指针存在了32位字段里。

最后还有4个字节是为了满足对齐需求而做的填充(padding)。

命令5、mem命令来看实际内存里的数据格式

我们执行 help 时发现已经没有 mem 命令了,那么现在只能通过 HSDB 可视化工具来获取信息。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

关于这块的讲解可以参考 R大的文章,文章中讲述还是使用 mem 命令,格式如下:mem 0x000000013316f118 2

mem 命令接受的两个参数都必选,一个是起始地址,另一个是以字宽为单位的“长度”。

虽然我们通过 inspect 命令是知道 Heat 实例有 16 字节,为什么给2暂不可知。

在实践的过程中,发现了一个类似的命令:

hsdb> examine 0x000000013316f118/2
0x000000013316f118: 0x0000000000000001 0x0000000116954620
           

命令6、revptrs 反向指针

JVM 通过引用来定位堆上的具体对象,有两种实现方式:句柄池和直接指针。目前 Java 默认使用的 HotSpot 虚拟机采用的便是直接指针进行对象访问的。

我们在执行 Java 程序时加了 UseCompressedOops 参数,即使不加,Java9 也会默认开启压缩指针。启用“压缩指针”的功能把64位指针压缩到只用32位来存。压缩指针与非压缩指针直接有非常简单的1对1对应关系,前者可以看作后者的特例。关于压缩指针,感兴趣的朋友可以阅读本文。

于是我们要找 heatStatic、heat、heatWay 这三个变量,等同于找出存有指向上述3个 Heat 实例的地址的存储位置。

不嫌麻烦的话手工扫描内存去找也能找到,不过幸好HSDB内建了revptrs命令,可以找出“反向指针”——如果a变量引用着b对象,那么从b对象出发去找a变量就是找一个“反向指针”。

hsdb> revptrs 0x000000013316f118
null
Oop for java/lang/Class @ 0x000000013316d660
           

确实找到了一个 Heat 实例的指针,在一个 java.lang.Class 的实例里。

用 findpc 命令来看看这个Class对象在哪里:

hsdb> findpc 0x000000013316d660
Address 0x000000013316d660: In thread-local allocation buffer for thread "main" (3587)  [0x00000001331639f8,0x000000013316f160,0x000000013318c730,{
      0x000000013318c970})
           

可以看到这个 Class 对象也在 eden 里,具体来说在 main 线程的 TLAB 里。

这个 Class 对象是如何引用到 Heat 的实例的呢?再用 inspect 命令:

hsdb> inspect 0x000000013316d660
instance of Oop for java/lang/Class @ 0x000000013316d660 @ 0x000000013316d660 (size = 184)
<<Reverse pointers>>: 
heatStatic: Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f118 Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f118
           

可以看到,这个 Class 对象里存着 Heat 类的静态变量 heatStatic,指向着第一个 Heat 实例。注意该对象没有对象头。

静态变量按照定义存放在方法区,虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆)。但现在在 JDK7 的 HotSpot VM 里它实质上也被放在 Java heap 里了。可以把这种特例看作是 HotSpot VM 把方法区的一部分数据也放在 Java heap 里了。

通过可视化工具操作也可以得到上述结果:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

最终得到同样的结果:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

同理,我们查找一下第二个变量 heat 的存储信息。

hsdb> revptrs 0x000000013316f140
null
Oop for com/msdn/java/hotspot/hsdb/Heat2 @ 0x000000013316f128
hsdb> findpc 0x000000013316f128
Address 0x000000013316f128: In thread-local allocation buffer for thread "main" (3587)  [0x00000001331639f8,0x000000013316f160,0x000000013318c730,{
      0x000000013318c970})
hsdb> inspect 0x000000013316f128
instance of Oop for com/msdn/java/hotspot/hsdb/Heat2 @ 0x000000013316f128 @ 0x000000013316f128 (size = 24)
<<Reverse pointers>>: 
_mark: 1
_metadata._klass: InstanceKlass for com/msdn/java/hotspot/hsdb/Heat2
heat: Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f140 Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f140
           

接着来找第三个变量 heatWay:

hsdb> revptrs 0x000000013316f150
null
null
           

回到我们的 HSDB 可视化界面,可以发现如下信息:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

Stack Memory 窗口的内容有三栏:

  • 左起第1栏是内存地址,提醒一下本文里提到“内存地址”的地方都是指虚拟内存意义上的地址,不是“物理内存地址”,不要弄混了这俩概念;
  • 第2栏是该地址上存的数据,以字宽为单位
  • 第3栏是对数据的注释,竖线表示范围,横线或斜线连接范围与注释文字。

仔细看会发现那个窗口里正好就有 0x000000013316f150 这数字,位于 0x00007000068e29e0 地址上,而这恰恰对应 main 线程上 generate()的栈桢。

3.2. JConsole:Java监视与管理控制台

3.2.1. jconsole简介

JConsole(java monitoring and management console)是一款基于JMX的可视化监视和管理工具。

3.2.2. 启动JConsole

  • 点击JDK/bin 目录下面的“jconsole.exe”即可启动
  • 然后会自动自动搜索本机运行的所有虚拟机进程
  • 选择其中一个进程可开始进行监控
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.2.3. JConsole基本介绍

JConsole 基本包括以下基本功能:概述、内存、线程、类、VM概要、MBean

运行下面的程序、然后使用JConsole进行监控;注意设置虚拟机参数

package com.jvm.jconsole;
import java.util.ArrayList;
import java.util.List;
/**
 * 设置虚拟机参数:-Xms100M -Xms100m -XX:+UseSerialGC -XX:+PrintGCDetails
 */
public class Demo1 {
    static class OOMObject {
        public byte[] placeholder = new byte[64 * 1024];
    }
    public static void fillHeap(int num) throws InterruptedException {
        Thread.sleep(20000); //先运行程序,在执行监控
        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);
        while (true) {
            //让其一直运行着
        }
    }
}           
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
  • 打开JConsole查看上面程序
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
  • 可以切换顶部的选项卡查看各种指标信息。
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.2.4. 内存监控

“内存”页签相当于可视化的jstat 命令,用于监视受收集器管理的虚拟机内存的变换趋势。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
  • 代码运行,控制台也会输出gc日志:
[GC (Allocation Failure) [DefNew: 27328K->3392K(30720K), 0.0112139 secs] 27328K->19901K(99008K), 0.0112664 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 30720K->3392K(30720K), 0.0133413 secs] 47229K->40117K(99008K), 0.0133708 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 30664K->3374K(30720K), 0.0140975 secs] 67389K->65091K(99008K), 0.0141239 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
[Full GC (System.gc()) [Tenured: 61716K->66636K(68288K), 0.0098835 secs] 66919K->66636K(99008K), [Metaspace: 9482K->9482K(1058816K)], 0.0100578 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]           

3.2.5. 线程监控

如果上面的“内存”页签相当于可视化的jstat命令的话,“线程”页签的功能相当于可视化的jstack命令,遇到线程停顿时可以使用这个页签进行监控分析。线程长时间停顿的主要原因主要有:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待(活锁和死锁)

下面三个方法分别等待控制台输入、死循环演示、线程锁等待演示

  • 第一步:运行如下代码:
package com.jvm.jconsole;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Demo2 {
    public static void main(String[] args) throws IOException {
        waitRerouceConnection();
        createBusyThread();
        createLockThread(new Object());
    }
    /**
     * 等待控制台输入
     *
     * @throws IOException
     */
    public static void waitRerouceConnection() throws IOException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
                try {
                    br.readLine();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }, "waitRerouceConnection");
        thread.start();
    }
    /**
     * 线程死循环演示
     */
    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();
    }
}           
  • 第二步:打开jconsole中查看上面程序运行情况,可以查看到3个目标线程
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
  • 第三步:查看目标线程信息

waitRerouceConnection线程处于读取数据状态,如下图:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

testBusyThread线程位于代码45行,处于运行状态,如下图:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
    • testLockThread处于活锁等待状态,如下图:
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

只要lock对象的notify()或notifyAll()方法被调用,这个线程便可能激活以继续执行

3.2..6. 线程死锁演示

第一步:运行下面代码:

package com.jvm.jconsole;

public class Demo3 {
    public static void main(String[] args) {
        User u1 = new User("u1");
        User u2 = new User("u2");
        Thread thread1 = new Thread(new SynAddRunalbe(u1, u2, 1, 2, true));
        thread1.setName("thread1");
        thread1.start();
        Thread thread2 = new Thread(new SynAddRunalbe(u1, u2, 2, 1, false));
        thread2.setName("thread2");
        thread2.start();
    }
    /**
     * 线程死锁等待演示
     */
    public static class SynAddRunalbe implements Runnable {
        User u1, u2;
        int a, b;
        boolean flag;
        public SynAddRunalbe(User u1, User u2, int a, int b, boolean flag) {
            this.u1 = u1;
            this.u2 = u2;
            this.a = a;
            this.b = b;
            this.flag = flag;
        }
        @Override
        public void run() {
            try {
                if (flag) {
                    synchronized (u1) {
                        Thread.sleep(100);
                        synchronized (u2) {
                            System.out.println(a + b);
                        }
                    }
                } else {
                    synchronized (u2) {
                        Thread.sleep(100);
                        synchronized (u1) {
                            System.out.println(a + b);
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static class User {
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public User(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}           

thread1持有u1的锁,thread2持有u2的锁,thread1等待获取u2的锁,thread2等待获取u1的锁,相互需要获取的锁都被对方持有者,造成了死锁。程序中出现了死锁的情况,我们是比较难以发现的。需要依靠工具解决。刚好jconsole就是这个美妙的工具。

第二步:在jconsole中打开上面程序的监控信息:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

从上面可以看出代码43行和50行处导致了死锁。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

关于程序死锁的,我们还可以使用命令行工具jstack来查看java线程堆栈信息,也可以发现死锁。

3.3. JVisualVM:多合一故障处理工具

3.3.1. VisualVM介绍

VisualVM 是一款免费的,集成了多个 JDK 命令行工具的可视化工具,它能为您提供强大的分析能力,对 Java 应用程序做性能分析和调优。这些功能包括生成和分析海量数据、跟踪内存泄漏、监控垃圾回收器、执行内存和 CPU 分析,同时它还支持在 MBeans 上进行浏览和操作。本文主要介绍如何使用 VisualVM 进行性能分析及调优。

VisualVM位于{JAVA_HOME}/bin目录中。

点击运行,效果如下:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.3.2. 查看jvm配置信息

  • 第一步:点击左边窗口显示正在运行的java进程
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
  • 第二步:点击右侧窗口“概述”,可以查看各种配置信息
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

通过jdk提供的jinfo命令工具也可以查看上面的信息。

3.3.3. 查看cpu、内存、类、线程监控信息

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.3.4. 查看堆的变化

步骤一:运行下面的代码

  • 每隔3秒,堆内存使用新增100M
package com.jvm.visualvm;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class Demo1 {
    public static final int _1M = 1024 * 1024;
    public static void main(String[] args) throws InterruptedException {
        List<Object> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            list.add(new byte[100 * _1M]);
            TimeUnit.SECONDS.sleep(3);
            System.out.println(i);
        }
    }
}           

步骤二:在VisualVM可以很清晰的看到堆内存变化信息。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.3.5. 查看堆快照

步骤一:点击“监视”->”堆(dump)”可以生产堆快照信息.

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

步骤二:生成了以heapdump开头的一个选项卡,内容如下:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

对于“堆 dump”来说,在远程监控jvm的时候,VisualVM是没有这个功能的,只有本地监控的时候才有。

3.3.6. 导出堆快照文件

步骤一:查看堆快照,此步骤可以参考上面的“查看堆快照”功能

步骤二:右键点击另存为,即可导出hprof堆快照文件,可以发给其他同事分析使用

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.3.7. 查看class对象加载信息

其次来看下永久保留区域PermGen使用情况

步骤一:运行一段类加载的程序,代码如下:

package com.jvm.visualvm;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class Demo2 {
    private static List<Object> insList = new ArrayList<Object>();
    public static void main(String[] args) throws Exception {
        permLeak();
    }
    private static void permLeak() throws Exception {
        for (int i = 0; i < 2000; i++) {
            URL[] urls = getURLS();
            URLClassLoader urlClassloader = new URLClassLoader(urls, null);
            Class<?> logfClass = Class.forName("org.apache.commons.logging.LogFactory", true, urlClassloader);
            Method getLog = logfClass.getMethod("getLog", String.class);
            Object result = getLog.invoke(logfClass, "TestPermGen");
            insList.add(result);
            System.out.println(i + ": " + result);
            if (i % 100 == 0) {
                TimeUnit.SECONDS.sleep(1);
            }
        }
    }
    private static URL[] getURLS() throws MalformedURLException {
        File libDir = new File("D:\\installsoft\\maven\\.m2\\repository3.3.9_0\\commons-logging\\commons-logging\\1.1.1");
        File[] subFiles = libDir.listFiles();
        int count = subFiles.length;
        URL[] urls = new URL[count];
        for (int i = 0; i < count; i++) {
            urls[i] = subFiles[i].toURI().toURL();
        }
        return urls;
    }
}           

步骤二:打开visualvm查看,metaspace

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.3.8. CPU分析:发现cpu使用率最高的方法

CPU 性能分析的主要目的是统计函数的调用情况及执行时间,或者更简单的情况就是统计应用程序的 CPU 使用情况。

没有程序运行时的 CPU 使用情况如下图:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

步骤一:运行下列程序:

package com.jvm.visualvm;

public class Demo3 {
    public static void main(String[] args) throws InterruptedException {
        cpuFix();
    }
    /**
    * cpu 运行固定百分比
    *
    * @throws InterruptedException
    */
    public static void cpuFix() throws InterruptedException {
        // 80%的占有率
        int busyTime = 8;
        // 20%的占有率
        int idelTime = 2;
        // 开始时间
        long startTime = 0;
        while (true) {
            // 开始时间
            startTime = System.currentTimeMillis();
            /*
            * 运行时间
            */
            while (System.currentTimeMillis() - startTime < busyTime) {
                ;
            }
            // 休息时间
            Thread.sleep(idelTime);
        }
    }
}           

步骤二:打开visualvm查看cpu使用情况,我的电脑是8核的,如下图:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

过高的 CPU 使用率可能是我们的程序代码性能有问题导致的。可以切换到“抽样器”对cpu进行采样,可以擦看到那个方法占用的cpu最高,然后进行优化。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

从图中可以看出cpuFix方法使用cpu最多,然后就可以进行响应的优化了。

3.3.9. 查看线程快照:发现死锁问题

Java 语言能够很好的实现多线程应用程序。当我们对一个多线程应用程序进行调试或者开发后期做性能调优的时候,往往需要了解当前程序中所有线程的运行状态,是否有死锁、热锁等情况的发生,从而分析系统可能存在的问题。

在 VisualVM 的监视标签内,我们可以查看当前应用程序中所有活动线程(Live threads)和守护线程(Daemon threads)的数量等实时信息。

可以查看线程快照,发现系统的死锁问题。

步骤一:运行下面的代码:

package com.jvm.visualvm;

public class Demo4 {
    public static void main(String[] args) {
        Obj1 obj1 = new Obj1();
        Obj2 obj2 = new Obj2();
        Thread thread1 = new Thread(new SynAddRunalbe(obj1, obj2, 1, 2, true));
        thread1.setName("thread1");
        thread1.start();
        Thread thread2 = new Thread(new SynAddRunalbe(obj1, obj2, 2, 1, false));
        thread2.setName("thread2");
        thread2.start();
    }
    /**
    * 线程死锁等待演示
    */
    public static class SynAddRunalbe implements Runnable {
        Obj1 obj1;
        Obj2 obj2;
        int a, b;
        boolean flag;
        public SynAddRunalbe(Obj1 obj1, Obj2 obj2, int a, int b, boolean flag) {
            this.obj1 = obj1;
            this.obj2 = obj2;
            this.a = a;
            this.b = b;
            this.flag = flag;
        }
        @Override
        public void run() {
            try {
                if (flag) {
                    synchronized (obj1) {
                        Thread.sleep(100);
                        synchronized (obj2) {
                            System.out.println(a + b);
                        }
                    }
                } else {
                    synchronized (obj2) {
                        Thread.sleep(100);
                        synchronized (obj1) {
                            System.out.println(a + b);
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static class Obj1 {
    }
    public static class Obj2 {
    }
}           

程序中:thread1持有obj1的锁,thread2持有obj2的锁,thread1等待获取obj2的锁,thread2等待获取obj1的锁,相互需要获取的锁都被对方持有者,造成了死锁。程序中出现了死锁的情况,我们是比较难以发现的。需要依靠工具解决。

步骤二:打开visualvm查看堆栈信息:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

点击dump,生成线程堆栈信息:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

可以看到“Found one Java-level deadlock”,包含了导致死锁的代码。

"thread2":
    at com.jvm.visualvm.Demo4$SynAddRunalbe.run(Demo4.java:50)
    - waiting to lock <0x00000007173d40f0> (a com.jvm.visualvm.Demo4$Obj1)
    - locked <0x00000007173d6310> (a com.jvm.visualvm.Demo4$Obj2)
    at java.lang.Thread.run(Thread.java:745)
"thread1":
    at com.jvm.visualvm.Demo4$SynAddRunalbe.run(Demo4.java:43)
    - waiting to lock <0x00000007173d6310> (a com.jvm.visualvm.Demo4$Obj2)
    - locked <0x00000007173d40f0> (a com.jvm.visualvm.Demo4$Obj1)
    at java.lang.Thread.run(Thread.java:745)           

上面这段信息可以看出,thread1持有Obj1对象的锁,等待获取Obj2的锁,thread2持有Obj2的锁,等待获取Obj1的锁,导致了死锁。

3.3.10. 总结

本文介绍了jdk提供的一款非常强大的分析问题的一个工具VisualVM,通过他,我们可以做一下事情:

  • 查看应用jvm配置信息
  • 查看cpu、内存、类、线程监控信息
  • 查看堆的变化
  • 查看堆快照
  • 导出堆快照文件
  • 查看class对象加载信息
  • CPU分析:发现cpu使用率最高的方法
  • 分析死锁问题,找到死锁的代码

3.4. Java Mission Control(JMC):可持续在线的监控工具

3.4.1.JMC的组成

使用 JMC可以监视和管理 Java 应用程序,不会导致相关工具类的大幅度性能开销,它使用为 Java 虚拟机 (JVM) 的普通自适应动态优化收集的数据。

主要部分

  • JVM浏览器:显示了正在运行的 Java 应用程序及其 JVM,每个JVM实例称为一个JVM连接。JVM浏览器允许用户列出并连接到本地和远程运行的 Java 应用。它能够使用 Java 发现协议(JDP)自动地发现本地和远程运行的 Java 进程。
  • JMX 控制台:能够通过 JMX 接口管理并监控 JDK ,实时收集并显示其特征。它提供了实时集合、堆使用情况、CPU 负载以及其他通过 MBeans 暴露的和在 MBean 服务器中注册的信息,并可通过托管 Bean (MBean) 更改一些运行时属性。还可以创建在特定事件上触发的规则 (例如,如果应用程序的 CPU 占用率达到了 90%,则发送电子邮件)。
  • JFR:提供了一种从操作系统层、JVM 和 Java 应用程序层收集事件的方式。收集的事件包括线程延时事件,例如休眠(sleep)、等待(wait)、锁竞争、I/O、GC 和方法分析。
Java Mission Control 插件使用 Java Management Extensions (JMX) 代理连接到 JVM

启动JMC后,连接某个本地应用后,出现如下界面:

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

远程连接JVM(通过JMX连接如果想要用jmc监控远程的JVM进程,配置方式和jvisualvm方式一一样即可)

本地连接比较简单这里就不在赘述,远程连接JVM,这里利用VMWare工具进行模拟,过程中遇到一些问题,值得注意的。

首先,远程机器被监控的程序需要开启调试端口,在执行java命令行中加入以下属性,属性没有以ssl安全认证方式连接的,案例中启动监听端口为7091

3.4.2JMX配置(被监控的远程Tomcat)

进入tomcat安装目录安装找到catalina.sh文件,在CATALINA_OPTS中增加一下配置:

-Dcom.sun.management.jmxremote=true 
-Djava.rmi.server.hostname=115.29.206.6 
-Dcom.sun.management.jmxremote.port=6666 
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.managementote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 

-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
           

配置成功之后我的CATALINA_OPTS为:

CATALINA_OPTS="-Xms1024m -Xmx6144m -XX:+HeapDumpOnOutOfMemoryError 
-XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-Dspring.profiles.active=production 
-Xloggc:/data/logs/gc-`date +"%Y-%m-%d_%H%M%S"`.log 
-XX:MaxPermSize=1024M 
-Dcom.sun.management.jmxremote=true 
-Djava.rmi.server.hostname=115.29.206.6 
-Dcom.sun.management.jmxremote.port=6666 
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.managementote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder"
           

3.4.3主要配置项说明:

  • -Djava.rmi.server.hostname=115.29.206.6:这个配置的值是远程tomcat服务器的外网ip。
  • -Dcom.sun.management.jmxremote.port=6666:这个是对外开放的端口,后面在配置客户端的时候需要用到这个。
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

创建完成后双击MBean或者右键–>打开JMX控制台,均能打开控制台

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.4.4展示面板

3.4.4.1概览

默认布局提供 CPU 和内存使用情况的概览。

概览:可以添加自定义图表,通过概览的加号”添加图表”实现;可以重置,通过”重置为默认控件”实现。

添加图表后,可以通过图表的加号添加相应的子项,并可以在图表上右键详细设置(如下图中的Test)

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.4.4.2 MBean浏览器

JMC使用托管Bean (MBean) 为监视和管理 Java 应用程序性能提供统一且一致的界面。MBean 是采用符合 JMX 规范的设计模式的托管对象。MBean 可以表示一个设备、一个应用程序或需要托管的任何资源。MBean 的管理界面由一组属性、操作和通知组成。

MBean 浏览器提供对所有已注册 MBean 的访问。MBean 在管理服务器中注册,后者可以通过与 Java Management Extensions (JMX) 兼容的客户机访问

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

要创建和注册新 MBean,请单击 MBean 面板加号图标。执行此操作会启动动态创建和注册新的 MBean 向导,提示为新 MBean 输入对象名和类名。若要继续,对象名必须有效,并且类名必须是有效的 Java 类名。请注意,该向导不会验证类是否对 MBean 服务器可用;将只进行语法检查。要注销特定 MBean,右键单击并从上下文菜单中选择注销。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

3.4.4.3 MBean功能

  • 属性:列出所选 MBean 的属性。
  • 操作:列出可从所选 MBean 调用的操作。
  • 通知:列出在运行时期间 MBean 所提示的通知。
  • 元数据:包含描述 MBean 的信息。

3.4.4.4 触发器

使用触发器选项卡可以管理满足特定条件时触发事件的规则。这是一种无需持续地监视应用程序即可跟踪运行时问题的有用方法。以灰色显示的规则在监视的 JVM 中不可用。默认情况下,停用所有规则。要激活某个规则,请选中该规则旁边的复选框

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

规则详细信息

  • 条件:选择触发规则时所在的属性和值。
  • 操作:选择应由规则触发的事件。
  • 约束条件:选择激活规则时的时间约束条件。

创建触发器

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

设置触发规则的条件:

  • 最大触发值:最大触发值(对于数值属性) 或匹配字符串 (对于非数值属性),指定用于触发规则的选定属性的最大值或匹配字符串。
  • 持续时间:指定条件必须保持为“真”以触发规则的持续时间 (默认秒)。
  • 限制时间段:指定可以接下来再次触发规则所要经过的最短时间长度 (默认秒)。
  • 满足条件时触发:选择是否在满足条件时触发规则。例如,如果触发值设置为 100,则在值从小于 100 更改为 100 或更高值时触发规则。
  • 从条件恢复时触发:选择是否当不再满足条件时触发规则。例如,如果触发值设置为 100,则在值从 100 或更高值更改为 100 时触发规则。
深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

在规则触发时发生的操作

  • 应用程序预警:在JMC中显示预警对话框。
  • 控制台输出:将通知发送到启动 JMX 控制台的控制台 (命令提示) 的标准输出 (stdout)。
  • 转储飞行记录:将最后指定时段 (秒) 的飞行记录中可用的全部数据转储到 JFR 文件。
  • HPROF 转储:启动到指定 HPROF 文件的内存转储。
  • 调用诊断命令:调用指定的诊断命令,通过附加或覆盖结果将输出记录到指定的 LOG 文件。
  • 记录到文件:将通知写入到指定的 TXT 日志文件。
  • 发送电子邮件:通过电子邮件发送通知。可以配置 SMTP 服务器地址和端口、电子邮件的接收方和发送方并提供 SSL 身份证明 (如果需要安全连接)。
  • 启动连续飞行记录:启动连续飞行记录。
  • 启动限时飞行记录:启动飞行记录,在指定的时段之后将结果转储到 JFR 文件中。

3.4.4.5系统

系统选项卡提供了运行 JVM 的系统的信息、JVM 的性能属性以及系统属性列表。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
  • 服务器信息

    在系统选项卡顶部的服务器信息面板中,包含运行 JVM 的服务器的类别和值列表。这些信息对调试应用程序开发问题和运行时问题以及提出支持请求非常有用。这是一般系统信息,不能更改。

  • JVM 统计信息

    JVM 统计信息面板包含 JVM 主要性能属性的当前值。默认情况下,表中显示以下属性:

  • 当前已加载类计数

    运行时间

要向表中添加属性,请单击 JVM 统计信息面板右上角的添加属性按钮。要删除属性,请在表中右键单击该属性,然后选择删除。右键单击属性后,可以更改其更新间隔、单位,而对于一些属性,还可以设置值。
  • 系统属性

    系统属性面板包含一个表,其中列出使用 JVM 调用的所有属性的关键字和值。要筛选属性,请选择筛选列 (关键字或值),并在表上方的文本字段中指定筛选器字符串。

    要配置表外观,请单击系统属性面板右上角的表设置按钮,然后选择要显示或隐藏的列。对于各列,可以设置最小宽度、比例和初始排序顺序。展示的是一般系统信息,不能更改。

3.4.4.6内存

使用内存选项卡可以监视应用程序使用内存资源的效率。此选项卡主要提供以下方面的信息:堆使用量、垃圾收集和活动内存池。此选项卡上提供的信息可帮助确定是否已将 JVM 配置为提供最佳应用程序性能。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
在内存选项卡中,可以使用该选项卡右上角的运行完全垃圾收集按钮手动启动完全垃圾收集。
  • GC 表

    GC 表面板包含可用垃圾收集器的主要性能属性的当前值。该面板分为标有活动垃圾收集器名称的多个选项卡

  • 总收集时间
  • 收集计数
  • GC 开始时间
  • GC 结束时间
  • GC 持续时间
  • GC ID
  • GC 线程计数
右键单击属性后,可以更改其更新间隔、单位,而对于一些属性,还可以设置值。
  • 活动内存池

    活动内存池面板包含一个表,其中列出可供 JVM 使用的内存池的信息。默认情况下,该表包含以下列:

  • 池名称:内存池的名称。
  • 类型:内存池的类型。如果内存池属于 Java 堆,则类型为 HEAP,否则为 NON_HEAP。
  • 已用:当前已用的内存池大小。
  • 最大值:内存池的最大大小。
  • 占用率:当前使用量占最大内存池大小的百分比。
  • 已用峰值:在受监视 JVM 的有效期内内存池已用内存的峰值。
  • 最大值峰值:在受监视 JVM 的有效期内最大内存池大小的峰值。

3.4.4.7线程

使用线程选项卡可以监视线程活动。此选项卡包含一个绘制应用程序随时间推移的活动线程使用情况的图形、一个由该应用程序使用的所有活动线程的表以及选定线程的堆栈跟踪。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
  • 活动线程图

    活动线程图显示 Java 应用程序随时间推移启动的线程数。默认情况下,图形中显示以下属性:

    • 高峰活动线程计数
    • 活动线程总计数
    • 守护程序活动线程计数
  • 活动线程

    活动线程面板包含一个表,其中列出 Java 应用程序所启动活动线程的信息。默认情况下,该表包含以下列:

    • 线程名称:线程的名称。
    • 线程状态:线程的状态。线程可以是以下状态之一:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 或 TERMINATED。
    • 受阻计数:线程处于 BLOCKED 状态的次数。
    • CPU 总体占用率:线程所使用 CPU 资源占总 CPU 资源的百分比。默认情况下不提取此值。要启用它,请在表上方选择 CPU 概要分析。
    • 死锁:线程是否已死锁。默认情况下不提取此值。要启用它,请在表上方选择死锁检测。
    • 已分配的字节:已分配给线程的字节数。默认情况下不提取此值。要启用它,请在表上方选择分配。
实时监视最后三个值会消耗大量系统资源。这就是默认情况下禁用它们的原因。使用表上方相应的复选框,对这些值启用监视。
  • 选定线程的堆栈跟踪

    在活动线程表中选择线程后,其整个堆栈跟踪将显示在下面的选定线程的堆栈跟踪面板中。堆栈跟踪包含所有方法 (一直到当前执行的方法) 的调用路径。它非常有用,例如,当需要确定导致死锁或代码执行过程中意外中断的方法时。

可以使用 Ctrl 键在活动线程表中选择多个线程来显示多个堆栈跟踪。

3.4.4.8诊断命令

使用诊断命令可监视 Java 应用程序的效率和性能。JMC 使用大量不同的诊断工具,包括一组可以使用诊断命令选项卡针对应用程序运行的命令。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全
运行 JMX 控制台监视 JVM 的额外成本很小,几乎可以忽略不计。它提供低成本的应用程序监视和概要分析 JMX标准参考

四. HotSpot虚拟机插件及工具

HotSpot虚拟机发展了二十余年,现在已经是一套很复杂的软件系统,如果深入挖掘HotSpot的源码,可以发现在HotSpot的研发过程中,开发团队曾经编写(或者收集)过不少虚拟机的插件和辅助工 具,它们存放在HotSpot源码hotspot/src/share/tools目录下,包括(含曾经有过但新版本中已被移除 的):

  • Ideal Graph Visualizer:用于可视化展示C2即时编译器是如何将字节码转化为理想图,然后转化为机器码的。
  • Client Compiler Visualizer[1]:用于查看C1即时编译器生成高级中间表示(HIR),转换成低级中间表示(LIR)和做物理寄存器分配的过程。
  • MakeDeps:帮助处理HotSpot的编译依赖的工具。
  • Project Creator:帮忙生成Visual Studio的.project文件的工具。
  • LogCompilation:将-XX:+LogCompilation输出的日志整理成更容易阅读的格式的工具。
  • HSDIS:即时编译器的反汇编插件。

HSDIS:JIT生成代码反汇编

HSDIS是一个被官方推荐的HotSpot虚拟机即时编译代码的反汇编插件,它包含在HotSpot虚拟机 的源码当中[2],在OpenJDK的网站[3]也可以找到单独的源码下载,但并没有提供编译后的程序。

HSDIS插件的作用是让HotSpot的-XX:+PrintAssembly指令调用它来把即时编译器动态生成的本地代码还原为汇编代码输出,同时还会自动产生大量非常有价值的注释,这样我们就可以通过输出的汇编代码来从最本质的角度分析问题。

另外还有一点需要注意,如果使用的是SlowDebug或者FastDebug版的HotSpot,那可以直接通 过-XX:+PrintAssembly指令使用的插件;如果使用的是Product版的HotSpot,则还要额外加入一 个-XX:+UnlockDiagnosticVMOptions参数才可以工作。

测试代码

public class Bar { 
	int a = 1; 
	static int b = 2; 
	public int sum(int c) { 
		return a + b + c; 
	}
	public static void main(String[] args) { 
		new Bar().sum(3); 
	} 
} 
12345678910           

编译这段代码,并使用以下命令执行:

java -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*Bar.sum -XX:Compile-Command=compileonly,*Bar.sum test.Bar
1           

其中,参数-Xcomp是让虚拟机以编译模式执行代码,这样不需要执行足够次数来预热就能触发即时编译。两个-XX:CompileCommand的意思是让编译器不要内联sum()并且只编译sum(),-XX:+PrintAssembly就是输出反汇编内容。

测试代码

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

虽然是汇编,但代码并不多,我们一句一句来阅读:

1)mov%eax,-0x8000(%esp):检查栈溢。

2)push%ebp:保存上一栈帧基址。

3)sub$0x18,%esp:给新帧分配空间。

4)mov 0x8(%ecx),%eax:取实例变量a,这里0x8(%ecx)就是ecx+0x8的意思,前面代码片段“[Constants]”中提示了“this:ecx=‘test/Bar’”,即ecx寄存器中放的就是this对象的地址。偏移0x8是越 过this对象的对象头,之后就是实例变量a的内存位置。这次是访问Java堆中的数据。

5)mov$0x3d2fad8,%esi:取test.Bar在方法区的指针。

6)mov 0x68(%esi),%esi:取类变量b,这次是访问方法区中的数据。

7)add%esi,%eax、add%edx,%eax:做2次加法,求a+b+c的值,前面的代码把a放在eax中,把b 放在esi中,而c在[Constants]中提示了,“parm0:edx=int”,说明c在edx中。

8)add$0x18,%esp:撤销栈帧。

9)pop%ebp:恢复上一栈帧。

10)test%eax,0x2b0100:轮询方法返回处的SafePoint。

11)ret:方法返回。

在这个例子中测试代码比较简单,肉眼直接看日志中的汇编输出是可行的,但在正式环境中-XX:+PrintAssembly的日志输出量巨大,且难以和代码对应起来,这就必须使用工具来辅助了。 JITWatch[5]是HSDIS经常搭配使用的可视化的编译日志分析工具,为便于在JITWatch中读取,读者可使用以下参数把日志输出到logfile文件:

-XX:+UnlockDiagnosticVMOptions

-XX:+TraceClassLoading

-XX:+LogCompilation

-XX:LogFile=/tmp/logfile.log

-XX:+PrintAssembly

-XX:+TraceClassLoading

在JITWatch中加载日志后,就可以看到执行期间使用过的各种对象类型和对应调用过的方法了, 界面如图4-28所示。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

选择想要查看的类和方法,即可查看对应的Java源代码、字节码和即时编译器生成的汇编代码, 如图4-29所示。

深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

继续阅读