15158846557 在线咨询 在线咨询
15158846557 在线咨询
所在位置: 首页 > 营销资讯 > 网站运营 > 深入理解JVM虚拟机——Java虚拟机的监控及诊断工具大全

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

时间:2023-06-28 02:21:01 | 来源:网站运营

时间:2023-06-28 02:21:01 来源:网站运营

深入理解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和名字:







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堆信息:







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参数设置的信息:







生成dump文件

jmap -dump:live,format=b,file=‪C:/Users/lx/Desktop/test1.bin 9472
将生成堆转储快照,这里我生成到桌面。后面可以使用jhat分析dump文件。







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

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







可以看到,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.HSDBsa-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 hsdbjhsdb 有 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 过程中会提示如下错误:







网上搜索相关解决方案,建议更换 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,如果按照上述步骤操作,是可以操作成功的。







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







更多操作选择推荐阅读:解读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





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

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

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

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

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

hsdb> helpAvailable 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 3587Thread 3587 Address: 0x00007fb25c00a800Java Stack Trace for mainThread 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> universeHeap 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: 0Gen 1: old [0x0000000133800000,0x0000000133800000,0x0000000142e00000) space capacity = 257949696, 0.0 usedInvocations: 0不借助命令的话,还可以这样操作来查看。










命令2、scanoops 查看类型

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

hsdb> scanoops 0x0000000132e00000 0x000000013318c970 com.msdn.java.hotspot.hsdb.Heat0x000000013316f118 com/msdn/java/hotspot/hsdb/Heat0x000000013316f140 com/msdn/java/hotspot/hsdb/Heat0x000000013316f150 com/msdn/java/hotspot/hsdb/Heatscanoops 接受两个必选参数和一个可选参数:必选参数是要扫描的地址范围,一个是起始地址一个是结束地址;可选参数用于指定要扫描什么类型的对象实例。实际扫描的时候会扫出指定的类型及其派生类的实例。

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




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

thread-local allocation buffer (TLAB)中

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

hsdb> whatis 0x000000012736efe8Unrecognized command. Try help...命令不行,那么换种思路,使用 HSDB 可视化窗口来查看对象的地址信息。







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

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


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

hsdb> inspect 0x000000013316f118instance 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 实例字段可显示。

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







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

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

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

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




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

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







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

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

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

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

hsdb> examine 0x000000013316f118/20x000000013316f118: 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 0x000000013316f118nullOop for java/lang/Class @ 0x000000013316d660确实找到了一个 Heat 实例的指针,在一个 java.lang.Class 的实例里。

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

hsdb> findpc 0x000000013316d660Address 0x000000013316d660: In thread-local allocation buffer for thread "main" (3587) [0x00000001331639f8,0x000000013316f160,0x000000013318c730,{ 0x000000013318c970})可以看到这个 Class 对象也在 eden 里,具体来说在 main 线程的 TLAB 里。

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

hsdb> inspect 0x000000013316d660instance 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 里了。

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







最终得到同样的结果:







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

hsdb> revptrs 0x000000013316f140nullOop for com/msdn/java/hotspot/hsdb/Heat2 @ 0x000000013316f128hsdb> findpc 0x000000013316f128Address 0x000000013316f128: In thread-local allocation buffer for thread "main" (3587) [0x00000001331639f8,0x000000013316f160,0x000000013318c730,{ 0x000000013318c970})hsdb> inspect 0x000000013316f128instance 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/Heat2heat: Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f140 Oop for com/msdn/java/hotspot/hsdb/Heat @ 0x000000013316f140接着来找第三个变量 heatWay:

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







Stack Memory 窗口的内容有三栏:

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




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

3.2.1. jconsole简介

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

3.2.2. 启动JConsole







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) { //让其一直运行着 } }}

















3.2.4. 内存监控

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







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





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







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













只要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中打开上面程序的监控信息:







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







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

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

3.3.1. VisualVM介绍

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

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

点击运行,效果如下:







3.3.2. 查看jvm配置信息













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

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







3.3.4. 查看堆的变化

步骤一:运行下面的代码

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可以很清晰的看到堆内存变化信息。







3.3.5. 查看堆快照

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







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







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

3.3.6. 导出堆快照文件

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

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













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







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

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

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







步骤一:运行下列程序:

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核的,如下图:







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







从图中可以看出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查看堆栈信息:







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







可以看到“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,通过他,我们可以做一下事情:

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

3.4.1.JMC的组成

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

主要部分

Java Mission Control 插件使用 Java Management Extensions (JMX) 代理连接到 JVM
启动JMC后,连接某个本地应用后,出现如下界面:







远程连接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主要配置项说明:







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







3.4.4展示面板

3.4.4.1概览

默认布局提供 CPU 和内存使用情况的概览。
概览:可以添加自定义图表,通过概览的加号”添加图表”实现;可以重置,通过”重置为默认控件”实现。
添加图表后,可以通过图表的加号添加相应的子项,并可以在图表上右键详细设置(如下图中的Test)







3.4.4.2 MBean浏览器

JMC使用托管Bean (MBean) 为监视和管理 Java 应用程序性能提供统一且一致的界面。MBean 是采用符合 JMX 规范的设计模式的托管对象。MBean 可以表示一个设备、一个应用程序或需要托管的任何资源。MBean 的管理界面由一组属性、操作和通知组成。
MBean 浏览器提供对所有已注册 MBean 的访问。MBean 在管理服务器中注册,后者可以通过与 Java Management Extensions (JMX) 兼容的客户机访问







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







3.4.4.3 MBean功能

3.4.4.4 触发器

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







规则详细信息

创建触发器







设置触发规则的条件:







在规则触发时发生的操作

3.4.4.5系统

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







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

3.4.4.6内存

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







在内存选项卡中,可以使用该选项卡右上角的运行完全垃圾收集按钮手动启动完全垃圾收集。
右键单击属性后,可以更改其更新间隔、单位,而对于一些属性,还可以设置值。

3.4.4.7线程

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







实时监视最后三个值会消耗大量系统资源。这就是默认情况下禁用它们的原因。使用表上方相应的复选框,对这些值启用监视。
可以使用 Ctrl 键在活动线程表中选择多个线程来显示多个堆栈跟踪。

3.4.4.8诊断命令

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







运行 JMX 控制台监视 JVM 的额外成本很小,几乎可以忽略不计。它提供低成本的应用程序监视和概要分析 JMX标准参考

四. HotSpot虚拟机插件及工具

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

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







虽然是汇编,但代码并不多,我们一句一句来阅读:
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所示。








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




来源



关键词:虚拟,诊断,工具,理解,深入

74
73
25
news

版权所有© 亿企邦 1997-2025 保留一切法律许可权利。

为了最佳展示效果,本站不支持IE9及以下版本的浏览器,建议您使用谷歌Chrome浏览器。 点击下载Chrome浏览器
关闭