之前未接触过 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 对象。
问题记录
attach 成功后搜索字节码文件,点击 create class 时报错
java.io.IOException: No such file or directory
错误原因:权限不够
解决方案:
1、首先尝试修改 jdk/bin 文件夹的权限,尝试无效;
2、命令改为:sudo jhsdb hsdb,最后成功得到字节码文件,然后去 jdk/bin 文件夹下可以看到一个新的文件夹,里面存放了我们想要的字节码文件。
JDB
Java调试器(JDB)是Java类在命令行中调试程序的工具。 它实现了 Java 平台调试器体系结构。 它有助于使用Java调试接口(JDI)检测和修复 Java 程序中的错误。
作为开发,我们都在本地调试过项目,一般都使用 IDEA 等工具,方便快捷。那如果没有 IDEA、Eclipse 等工具时,只给你一份代码和配置好的 Java 环境,又该如何进行调试呢?
JDB使用
验证 JDB 安装
% jdb -version
这是jdb版本 9.0 (Java SE 版本 9.0.4)
JDB 语法
jdb [ options ] [ class ] [ arguments ]
它从 Java Development Kit 调用 jdb.exe。
-
options
其中包括用于以高效方式调试Java程序的命令行选项。 JDB启动程序接受所有选项(例如-D,-classpath和-X)以及一些其他高级选项,例如(-attach,-listen,-launch等)。执行 jdb -help 可以看到各参数详细介绍,该部分最为重要。
-
class
在其上执行调试操作的类名。
-
arguments
这些是在运行时为程序提供的输入值。 例如,arg [0],arg [1]到main()方法。
调试命令
执行 help 可以查看所有可执行的命令,介绍一下常用的命令:
1、添加断点
stop at com.msdn.MyClass:22
stop in com.msdn.MyClass.<init>#构造函数
stop in com.msdn.MyClass.<clinit>#静态代码块
需要注意的是上述语句在 windows 上可以执行,mac 上不需要包名,直接使用文件名。
2、调试
step #执行当前行
step up #一直执行, 直到当前方法返回到其调用方
stepi #执行当前指令
next #步进一行 (调用)
cont #从断点处继续执行
3、查看变量
print <expr> #输出表达式的值
dump <expr> #输出所有对象信息
eval <expr> #对表达式求值 (与 print 相同)
set <lvalue> = <expr> #向字段/变量/数组元素分配新值
locals #输出当前堆栈帧中的所有本地变量
4、其他
list [line number|method] -- 输出源代码
use (或 sourcepath) [source file path] #显示或更改源路径(目录)
run #运行
本机调试
进入 class 文件所在目录
jdb -XX:+UseSerialGC -Xmn10M com.msdn.java.hotspot.hsdb.HeatTest2 --启动jdb,可带参数
在 windows 上可以用上述带包名的形式进行调试。
jdb -XX:+UseSerialGC -Xmn10M HeatTest
在 Mac 上不需要包名。
测试案例
注意:Java 文件不需要包名,打开命令行窗口进入该文件所在目录,然后进行之后的操作。
public class HeatTest2 {
private static Test heatStatic = new Test();
private Test heat = new Test();
public void generate() {
Test heatWay = new Test();
}
public static void main(String[] args) {
HeatTest2 heatTest = new HeatTest2();
heatTest.generate();
}
}
class Test {
private String name;
}
首先编译 Java 源文件,
javac -g HeatTest2.java
进入 class 文件所在目录,然后启动 JDB
jdb -XX:+UseSerialGC -Xmn10M HeatTest2
> help
#执行 help 可以查看所有可执行的命令,这里就不一一列举了
> stop in HeatTest2.main
正在延迟断点HeatTest2.main。
将在加载类后设置。
> run
运行HeatTest2
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
>
VM 已启动: 设置延迟的断点HeatTest2.main
断点命中: "线程=main", HeatTest2.main(), 行=19 bci=0
19 HeatTest2 heatTest = new HeatTest2();
main[1] step
>
已完成的步骤: "线程=main", HeatTest2.<init>(), 行=8 bci=0
8 public class HeatTest2 {
main[1] step
>
已完成的步骤: "线程=main", HeatTest2.<init>(), 行=11 bci=4
11 private Test heat = new Test();
main[1] next
>
已完成的步骤: "线程=main", HeatTest2.main(), 行=19 bci=7
19 HeatTest2 heatTest = new HeatTest2();
main[1] next
>
已完成的步骤: "线程=main", HeatTest2.main(), 行=20 bci=8
20 heatTest.generate();
main[1] next
>
已完成的步骤: "线程=main", HeatTest2.main(), 行=21 bci=12
21 }
main[1] next
>
应用程序已退出
如果我们想要在 generate 方法中设置断点,则可以这样做:
> stop at HeatTest2:14
正在延迟断点HeatTest2:14。
将在加载类后设置。
> run
运行HeatTest2
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
>
VM 已启动: 设置延迟的断点HeatTest2:14
断点命中: "线程=main", HeatTest2.generate(), 行=14 bci=0
14 Test heatWay = new Test();
main[1] next
>
已完成的步骤: "线程=main", HeatTest2.generate(), 行=15 bci=8
15 }
main[1] next
>
已完成的步骤: "线程=main", HeatTest2.main(), 行=21 bci=12
21 }
main[1] next
>
应用程序已退出
JVM系列阅读