本文为《深入学习 JVM 系列》第七篇文章
之前未接触过 HSDB 工具,在深入学习反射时,研究其源码时需要了解生成的字节码文件,恰巧看到别人使用了 HSDB 工具,因此花时间学习了一番。
HSDB(Hotspot Debugger),是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除。
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。
HSDB实操
启动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 命令。
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
分析对象存储区域
下面代码中的 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 是多少:
% jps 9977 HeatTest 复制代码
接着我们按照上文讲解启动 HSDB,注意在 IDEA 中执行代码时,Java 版本为 Java9,要与 HSDB 相关的 Java 版本一致。接下来的操作步骤可以参考 R大的文章,或者其他相似的文章。
在 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> 复制代码
关于上述命令的讲解可以参考本文。
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 复制代码
不借助命令的话,还可以这样操作来查看。
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 可视化窗口来查看对象的地址信息。
至于为什么无法使用 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 实例字段可显示。
或者通过可视化工具来查看:
一个 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/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 里了。
关于静态变量的存储位置,如果想深入研究,可以参考本文。
通过可视化工具操作也可以得到上述结果:
最终得到同样的结果:
同理,我们查找一下第二个变量 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 可视化界面,可以发现如下信息:
Stack Memory 窗口的内容有三栏:
- 左起第1栏是内存地址,提醒一下本文里提到“内存地址”的地方都是指虚拟内存意义上的地址,不是“物理内存地址”,不要弄混了这俩概念;
- 第2栏是该地址上存的数据,以字宽为单位
- 第3栏是对数据的注释,竖线表示范围,横线或斜线连接范围与注释文字。
仔细看会发现那个窗口里正好就有 0x000000013316f150 这数字,位于 0x00007000068e29e0 地址上,而这恰恰对应 main 线程上 generate()的栈桢。
关于静态变量、成员变量和局部变量的存储位置,我们通过上文查看 JVM 底层数据可以验证我们最初的回答。
关于成员变量 heat,还有一种验证方式。
1、首先获取 Heat2 对象的地址
hsdb> scanoops 0x0000000132e00000 0x000000013318c970 com.msdn.java.hotspot.hsdb.Heat2 0x000000013316f128 com/msdn/java/hotspot/hsdb/Heat2 复制代码
2、inspect 该地址,可以看到其包含了 heat 对象。