JHSDB 是 JDK 9 中才正式提供的,它与 jcmd 类似是一个集成式的多功能工具箱。JHSDB 是一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具。

服务性代理是 HotSpot 虚拟机中一组用于映射 Java 虚拟机运行信息的、主要基于 Java 语言(含少量 JNI 代码)实现的 API 集合。服务性代理以 HotSpot 内部的数据结构为参照物进行设计,把这些 C++ 的数据抽象出 Java 模型对象,相当于 HotSpot 的 C++ 代码的一个镜像。通过服务性代理的 API,可以在一个独立的 Java 虚拟机的进程里分析其他 HotSpot 虚拟机的内部数据,或者从 HotSpot 虚拟机进程内存中 dump 出来的转储快 照里还原出它的运行状态细节。

启动

1)程序执行后通过 jps 查询到测试程序的进程 ID,具体如下:
image.png
2)使用以下命令进入 JHSDB 的图形化模式,并附加进程
image.png

功能介绍

HSDB 启动后主界面如下:
image.png
HSDB 链接上一个 Java 进程后,出现的第一窗口界面列出了目标 JVM 的所有线程,选择一个 Java 线程后,该界面的左上角有几个操作图标,可以显示选定线程的信息:

1. 线程信息

1)检视线程(Inspect Thread)
该操作显示对象检视器窗口,给出线程对象的 VM 的中间表示。在 JIT 编译过程中,需要把 Java 字节码转换成自己的中间表示,然后做一些优化,最终生成机器码与相关元数据。对象检视器既能查看 Java 对象,又支持查看虚拟机内部的 C++ 结构体信息。如下图所示:
image.png
Inspector 为我们展示了对象头和指向对象元数据的指针,里面包括了 Java 类型的名字、继承关系、实现接口关系,字段信息、方法信息、运行时常量池的指针、内嵌的虚方法表(vtable)以及接口方法表(itable)等。

2)栈内存(Stack Memory)
image.png

3)栈调用路径(Stack Trace)
显示线程的栈调用路径信息,能看到方法名称和地址,点击还可以超链到方法的详细信息。
image.png

4)线程信息(Thread Information)
image.png

2. 工具栏

此外,在 Tools 栏中也提供了很多实用的工具,这些工具有些用在调试方面,有些能帮助定位问题。
image.png

2.1 Class Browser

利用类浏览器(Class Browser)能查看所有目标虚拟机中载入的类,也能把选中的类导出为类文件。在遇到没有对应源码,又需要分析问题的时候,可以利用这个类浏览器导出类文件来查看代码。另外,比如遇到 OOM 异常,有可能是载入了太多的类,可以利用这个工具分析有哪些不需要载入的类,也可以用来分析那些希望载入但却漏掉了的类。
image.png

2.2 Code Viewer

代码查看器能展示方法的字节码和 JIT 编译器编译过之后生成的机器码。该功能能快速定位与 JIT 编译有关的问题。我们常常在 Server 模式和 Client 模式下遇到意想不到的异常,有的甚至会导致虚拟机崩溃。通过查看解析后的编译码,可以分析出问题的根本原因。
截屏2021-11-16 下午1.46.39.png

2.3 Compute Reverse Ptrs

计算反向指针功能能够计算出从 GC 根能够到达的对象的引用路径,引用路径的存在是确保不会从 Java 堆中把对象清除掉。当计算完反向指针后,对象检视窗口中的反向指针字段就会显示这个对象的反向指针:
截屏2021-11-16 下午1.14.44.png

2.4 Deadlock Detection

死锁检测功能能够检测 Java 线程中的死锁,如 VM 的线程中存在 Java 层死锁,该工具会打印线程死锁的信息和他们等的锁。
image.png

2.5 Find Value In Heap

该工具会列出一个特定值在 Java 堆中的所有位置,比如你想找到某个对象在 Java 堆中所有被引用的地址。

  • 地址指一个内存地址,代表一段内存空间,一般一个对象会有实际的一段内存空间
  • 一个对象会在很多地方被引用,这些地方就称之为位置

image.png
这个功能非常有助于弄清楚 GC 相关的问题,比如在某些场景下,一些对象会被回收导致 GC 崩溃,并且虚拟机在尝试访问这些对象的内存地址时也会崩溃。通过间接跟踪该对象的引用指针,可以得到为什么对象未曾被 GC 标记为存活以至于提前回收的线索。

2.6 Heap Parameters

堆要素工具显式了 Java 堆中不同代的内存地址边界,该功能有助于找到特定的地址所在的堆范围:
截屏2021-11-16 下午1.47.27.png
由于 G1 没有物理上的分代概念,所以整个 Java 堆是一块内存。指定 -XX:+UseSerialGC 参数后再次查看:
截屏2021-11-16 下午1.59.07.png

2.7 Monitor Cache Dump

该功能能够 dump 对象监视器缓存,在处理分析并发和同步相关的问题时会非常有用。Object Monitor 是并发编程中处理线程互斥和等待的实现机制,Monitor 一般由锁(mutex)对象和条件变量构成。
截屏2021-11-16 下午1.36.52.png

2.8 Object Histogram

利用对象直方图工具,我们会看到当前堆内存中对象的直方图,常用于诊断内存泄露和 OOM 类问题。
截屏2021-11-16 下午1.04.35.png
该工具也能显示指定类的实例,通过选中一个 class 后单击右上角的搜索按钮:
截屏2021-11-16 下午1.05.15.png
选中任何一个实例,就能看到所有能到达对象的访问路径,以及该实例的 GC 状态、存活或者待回收等:
截屏2021-11-16 下午1.06.04.png
截屏2021-11-16 下午1.06.35.png
GC 利用 oopmap 标记所有位置的数据是否是指向 GC 堆的引用,上图说明该实例对象有 2 个引用,可以通过 2 个不同的路径到达数据位置。

使用示例

下面我们借助 JHSDB 来分析以下代码,并通过实验来回答一个简单问题:staticObj、instanceObj、localObj 这三个变量本身(而不是它们所指向的对象)存放在哪里?

  1. /**
  2. * 由于JHSDB本身对压缩指针的支持存在很多缺陷,建议用64位系统的读者在实验时禁用压缩指针
  3. *
  4. * VM Args:-Xmx10m -XX:+UseSerialGC -XX:-UseCompressedOops
  5. */
  6. public class JHSDB_TestCase {
  7. static class Test {
  8. static ObjectHolder staticObj = new ObjectHolder();
  9. ObjectHolder instanceObj = new ObjectHolder();
  10. void foo() {
  11. ObjectHolder localObj = new ObjectHolder();
  12. // 这里打断点,使得程序暂停下来,这样方便进行分析
  13. System.out.println("done");
  14. }
  15. }
  16. private static class ObjectHolder {}
  17. public static void main(String[] args) {
  18. Test test = new Test();
  19. test.foo();
  20. }
  21. }

理论上 staticObj 随着 Test 的类型信息存放在方法区,instanceObj 随着 Test 的对象实例存放在 Java 堆中,而 localObj 则是存放在 foo() 方法栈帧的局部变量表中。下面我们通过 JHSDB 来验证这一点:

在代码中,运行至断点位置一共会创建三个 ObjectHolder 对象的实例,只要是对象实例必然会在 Java 堆中进行分配,那我们就先从这三个对象开始着手,把他们从 Java 堆中找出来。

因为启动时指定使用 Serial 收集器,所以通过 Tools->Heap Parameters,我们看到了典型的 Serial 的分代内存布局:新生代的 Eden、S1、S2 和老年代容量以及它们的虚拟内存地址起止范围。
image.png
通过区域的内存地址范围,打开 Windows->Console 窗口,使用 scanoops 命令在 Java 堆的新生代范围内查找 ObjectHolder 的实例(从 Eden 的起始地址到 To Survivor 结束地址)。
image.png
果然我们找到了三个实例的地址,且它们的地址都落到了 Eden 的范围之内,也顺带验证了一般情况下新对象在 Eden 中创建的分配规则。下面再使用 Tools->Inspector 确认下这三个地址中存放的对象:
image.png
由于我们的确没有在 ObjectHolder 上定义过任何字段,所以图中并没有看到任何实例字段数据。

接下来根据堆中对象实例地址找出引用它们的指针,打开 Windows->Console 窗口使用 revptrs 命令。
image.png
果然找到了一个引用该对象的地方,是在一个 JHSDB_TestCase 的 Test 内部类的实例里,并且给出了这个实例的地址,通过 Inspector 查看该对象实例,可以清楚看到这是一个 Test 类型的对象实例,并且有一个名为 instanceObj 的实例字段。
image.png
接下来我们查找第二个对象实例,从图中看到引用该对象的地方是一个 java.lang.Class 类型的对象实例,里面有一个名为 staticObj 的实例字段。
image.png
从《Java 虚拟机规范》所定义的概念模型来看,所有 Class 相关的信息都应该存放在方法区之中,但方法区该如何实现,在《Java 虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK 7 及其以后版本的 HotSpot 虚拟机选择把静态变量与类型在 Java 语言一端的映射 Class 对象存放在一起,存储于堆中,从我们的实验中也明确验证了这一点。

但当我们采用相同方法查找第三个 ObjectHolder 实例时,使用 revptrs 命令返回了一个 null,看来 revptrs 命令并不支持查找栈上的指针引用。不过因为我们代码足够简洁,可以人工在 Java Thread 窗口中选中 main 线程后点击 Stack Memory 按钮来查看该线程的栈内存:
image.png
从图中可以看到,main 线程的栈内存中确实引用了一个来自新生代的 JHSDB_TestCase$ObjectHolder 对象。