深入理解java虚拟机(十二)JHSDB:基于服务性代理的调试工具


前言

JHSDB虽然是JDK 9中才正式提供,但之前已经以sa-jdi.jar包里面的HSDB(可视化工
具)和CLHSDB(命令行工具)的形式存在了很长一段时间。它们两个都是JDK的正式成员,随着JDK一同发布,无须独立下载,使用也是完全免费的。

JDK中提供了JCMD和JHSDB两个集成式的多功能工具箱,它们由于有着“后发优势”,能够做得往往比之前的老工具们更好、更强大,下表所示是JCMD、 JHSDB与原基础工具实现相同功能的简要对比。

在这里插入图片描述
本篇的主题是可视化的故障处理,所以JCMD及JHSDB的命令行模式就不再作重点讲解了,接下来我们通过一个实验来讲解JHSDB的图形模式下的功能。


一、JHSDB介绍

JHSDB是一款基于服务性代理( Serviceability Agent, SA)实现的进程外调试工具。服务性代理是HotSpot虚拟机中一组用于映射Java虚拟机运行信息的、主要基于Java语言(含少量JNI代码)实现的API集合。服务性代理以HotSpot内部的数据结构为参照物进行设计,把这些C++的数据抽象出Java模型对象,相当于HotSpot的C++代码的一个镜像。通过服务性代理的API,可以在一个独立的Java虚拟机的进程里分析其他HotSpot虚拟机的内部数据,或者从HotSpot虚拟机进程内存中dump出来的转储快照里还原出它的运行状态细节。服务性代理的工作原理跟Linux上的GDB或者Windows上的Windbg是相似的。现在,我们要借助JHSDB来分析一下代码清单中的代码,并通过实验来回答一个简单问题: staticObj、 instanceObj、 localObj这三个变量本身(而不是它们所指向的对象)存放在哪里?

/**
* staticObj、 instanceObj、 localObj存放在哪里?
*/
public class JHSDB_TestCase {
	static class Test {
		static ObjectHolder staticObj = new ObjectHolder();
		ObjectHolder instanceObj = new ObjectHolder();
		void foo() {
			ObjectHolder localObj = new ObjectHolder();
			System.out.println("done"); // 这里设一个断点
		}
	}
	private static class ObjectHolder {}
	public static void main(String[] args) {
		Test test = new JHSDB_TestCase.Test();
		test.foo();
	}
}

答案当然都知道: staticObj随着Test的类型信息存放在方法区, instanceObj随着Test的对象实例存放在Java堆, localObject则是存放在foo()方法栈帧的局部变量表中,现在要做的是通过JHSDB来实践验证这一点。


二、JHSDB实操

首先,我们要确保这三个变量已经在内存中分配好,然后将程序暂停下来,以便有空隙进行实
验,这只要把断点设置在代码中加粗的打印语句上,然后在调试模式下运行程序即可。由于JHSDB本身对压缩指针的支持存在很多缺陷,建议用64位系统的读者在实验时禁用压缩指针,另外为了后续操作时可以加快在内存中搜索对象的速度,也建议读者限制一下Java堆的大小。本例中,笔者采用的运行参数如下:

-Xmx10m -XX:+UseSerialGC -XX:-UseCompressedOops

程序执行后通过jps查询到测试程序的进程ID,具体如下:

jps -l
8440 org.jetbrains.jps.cmdline.Launcher
11180 JHSDB_TestCase
15692 jdk.jcmd/sun.tools.jps.Jps

使用以下命令进入JHSDB的图形化模式,并使其附加进程11180:

jhsdb hsdb --pid 11180

命令打开的JHSDB的界面如图所示。
在这里插入图片描述
阅读上述JHSDB_TestCase类代码清单可知,运行至断点位置一共会创建三个ObjectHolder对象的实例,只要是对象实例必然会在Java堆中分配,既然我们要查找引用这三个对象的指针存放在哪里,不妨从这三个对象开始着手,先把它们从Java堆中找出来。

首先点击菜单中的Tools->Heap Parameters,结果如图所示,因为笔者的运行参数中指定了使用的是Serial收集器,图中我们看到了典型的Serial的分代内存布局, Heap Parameters窗口中清楚列出了新生代的Eden、 S1、 S2和老年代的容量(单位为字节)以及它们的虚拟内存地址起止范围。
在这里插入图片描述
如果不指定收集器,即使用JDK默认的G1的话,得到的信息应该类似如下所示:

Heap Parameters:
garbage-first heap [0x00007f32c7800000, 0x00007f32c8200000] region size 1024K

请读者注意一下图中各个区域的内存地址范围,后面还要用到它们。打开Windows->Console窗口,使用scanoops命令在Java堆的新生代(从Eden起始地址到To Survivor结束地址)范围内查找ObjectHolder的实例,结果如下所示:

hsdb>scanoops 0x00007f32c7800000 0x00007f32c7b50000 JHSDB_TestCase$ObjectHolder
0x00007f32c7a7c458 JHSDB_TestCase$ObjectHolder
0x00007f32c7a7c480 JHSDB_TestCase$ObjectHolder
0x00007f32c7a7c490 JHSDB_TestCase$ObjectHolder

果然找出了三个实例的地址,而且它们的地址都落到了Eden的范围之内,算是顺带验证了一般情况下新对象在Eden中创建的分配规则。再使用Tools->Inspector功能确认一下这三个地址中存放的对象,结果如图所示。

在这里插入图片描述
Inspector为我们展示了对象头和指向对象元数据的指针,里面包括了Java类型的名字、继承关
系、实现接口关系,字段信息、方法信息、运行时常量池的指针、内嵌的虚方法表(vtable)以及接口方法表(itable)等。由于我们的确没有在ObjectHolder上定义过任何字段,所以图中并没有看到任何实例字段数据,读者在做实验时不妨定义一些不同数据类型的字段,观察它们在HotSpot虚拟机里面是如何存储的。

接下来要根据堆中对象实例地址找出引用它们的指针,原本JHSDB的Tools菜单中有Compute
Reverse Ptrs来完成这个功能,但在笔者的运行环境中一点击它就出现Swing的界面异常,看后台日志是报了个空指针,这个问题只是界面层的异常,跟虚拟机关系不大,所以笔者没有继续去深究,改为使用命令来做也很简单,先拿第一个对象来试试看:

hsdb> revptrs 0x00007f32c7a7c458
Computing reverse pointers...
Done.
Oop for java/lang/Class @ 0x00007f32c7a7b180

果然找到了一个引用该对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址,通过Inspector查看该对象实例,可以清楚看到这确实是一个java.lang.Class类型的对象实例,里面有一个名为staticObj的实例字段,如图所示。

在这里插入图片描述
从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。 JDK 7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中,从我们的实验中也明确验证了这一点。接下来继续查找第二个对象实例:

hsdb>revptrs 0x00007f32c7a7c480
Computing reverse pointers...
Done.
Oop for JHSDB_TestCase$Test @ 0x00007f32c7a7c468

这次找到一个类型为JHSDB_TestCase$Test的对象实例,在Inspector中该对象实例显示如图所示。
在这里插入图片描述

这个结果完全符合我们的预期,第二个ObjectHolder的指针是在Java堆中JHSDB_TestCase$Test对象的instanceObj字段上。但是我们采用相同方法查找第三个ObjectHolder实例时, JHSDB返回了一个null,表示未查找到任何结果:

hsdb> revptrs 0x00007f32c7a7c490
null

看来revptrs命令并不支持查找栈上的指针引用,不过没有关系,得益于我们测试代码足够简洁,人工也可以来完成这件事情。在Java Thread窗口选中main线程后点击Stack Memory按钮查看该线程的栈内存,如图所示。

在这里插入图片描述
这个线程只有两个方法栈帧,尽管没有查找功能,但通过肉眼观察在地址0x00007f32e771c998上的值正好就是0x00007f32c7a7c490,而且JHSDB在旁边已经自动生成注释,说明这里确实是引用了一个来自新生代的JHSDB_TestCase$ObjectHolder对象。至此,本次实验中三个对象均已找到,并成功追溯到引用它们的地方,也就实践验证了开篇中提出的这些对象的引用是存储在什么地方的问题。

JHSDB提供了非常强大且灵活的命令和功能,本篇的例子只是其中一个很小的应用,我们在实际开发、学习时,可以用它来调试虚拟机进程或者dump出来的内存转储快照,以积累更多的实际经验。


结尾

  • 感谢大家的耐心阅读,如有建议请私信或评论留言。
  • 如有收获,劳烦支持,关注、点赞、评论、收藏均可,博主会经常更新,与大家共同进步
  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xd聊架构

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值