JVM系列之:关于HSDB的一点心得(一)

简介: JVM系列之:关于HSDB的一点心得(一)

1.jpg


本文为《深入学习 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 过程中会提示如下错误:


2.jpg


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


3.jpg


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


4.jpg


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

5.jpg


然后在 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
复制代码


不借助命令的话,还可以这样操作来查看。


6.jpg


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 可视化窗口来查看对象的地址信息。


7.jpg


至于为什么无法使用 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 实例字段可显示。


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


8.jpg


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


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


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


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


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


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


9.jpg


关于这块的讲解可以参考 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 里了。


关于静态变量的存储位置,如果想深入研究,可以参考本文


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


10.jpg


最终得到同样的结果:


1.jpg


同理,我们查找一下第二个变量 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 可视化界面,可以发现如下信息:


12.jpg


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 对象。


13.jpg

目录
相关文章
|
7月前
|
存储 缓存 Java
JVM简单总结
Java运行时数据区包括:程序计数器、虚拟机栈、本地方法栈、堆空间和方法区(元空间)。这些区域各自承担不同的功能,如存储局部变量、方法调用信息、对象实例及运行时常量池等。其中,堆空间分为伊甸园、幸存者和老年代区域,方法区则包含类型信息、静态变量等。
|
3月前
|
存储 Java Unix
深入理解JVM(三)
深入理解JVM(三)
|
Java
JVM
JVM
84 0
|
7月前
|
存储 算法 Java
|
存储 Java
JVM的组成
JVM的组成是为了提供一个独立于硬件和操作系统的执行环境,使得Java程序能够在不同的平台上运行。通过类加载器加载字节码,运行时数据区存储程序的运行时数据,执行引擎执行字节码指令,垃圾回收器管理内存,本地方法接口与本地库交互,从而实现Java程序的执行。这些组成部分相互配合,共同构成了JVM的功能和特性。
61 0
|
7月前
|
存储 Java Linux
|
7月前
|
存储 安全 前端开发
|
7月前
|
存储 Oracle Java
一文带你认识JVM
一文带你认识JVM
102 0
|
7月前
|
缓存 算法 Java
【每日一面】关于JVM
【每日一面】关于JVM
48 0