ღゝ◡╹)ノ❤️

集中一点,登峰造极!

  menu
137 文章
0 浏览
0 当前访客
ღゝ◡╹)ノ❤️

hsdb分析运行时数据 置顶!

问题引出

来一段代码

public class Test {
    static Tes t1 = new Tes();
    Tes t2 = new Tes();

    public void f() {
        Tes t3 = new Tes();
    }
}
class Tes{}

问:t1,t2,t3这三个变量(不是所指向的对象,对象本体肯定在堆里)到底存在哪?

刚学完jvm的童鞋可能会这么回答:

  • t1在存Java静态变量的地方,概念上在JVM的方法区(method area)里
  • t2在Java堆里,作为Test的一个实例的字段存在
  • t3在Java线程的调用栈里,作为Test.f()的一个局部变量存在

真的是这样的吗?我们要怎么证明呢?咱们借助hsdb这个好用的工具好好的分析一下。

准备

首先准备这样两份代码:

public class Test {
    static Tes t1 = new Tes();
    Tes t2 = new Tes();

    public void f() {
        Tes t3 = new Tes();
    }
}
class Tes{}
public class Test2 {
    public static void main(String[] args) {
        Test test =new Test();
        test.f();
    }
}

在我电脑上使用java version "1.8.0_181"版本的jdk,分析内存的时候会出现这样的错误:

image.png

暂且不知道是我自己的问题还是jdk的问题,于是我就去Oracle官网上找到jdk11,再使用hsdb发现还是会出现这个错误,于是我又换到了jdk17,这次就没问题了。

第一步 设置断点

首先把我们的代码用javac编译一下。

我们要想分析,肯定不能让代码立即执行完,于是我们可以借助jdk自带的jdb工具。

为了方便后续步骤,启动jdb的时候可以设定让目标Java程序使用serial GC,要不然jdk17默认的是g1,不太好查看。

启动jdb之后可以用stop in命令在指定的Java方法入口处设置断点,
然后用run命令指定主类名称来启动Java程序,
等跑到断点看看位置是否已经到满足需求,还没到的话可以用step、next之类的命令来向前进。
对jdb命令不熟悉的同学可以在启动jdb之后使用help命令来查看命令列表和说明。

具体步骤如下:

E:\IDEA_daiMa\sjjg\hsdb\java>jdb -XX:+UseSerialGC
正在初始化jdb...
> stop in Test.f
正在延迟断点Test.f。
将在加载类后设置。
> run Test2
运行 Test2
设置未捕获的java.lang.Throwable
设置延迟的未捕获的java.lang.Throwable
>
VM 已启动: 设置延迟的断点Test.f

断点命中: "线程=main", Test.f(), 行=6 bci=0
6            Tes t3 = new Tes();

main[1] next
>
已完成的步骤: "线程=main", Test.f(), 行=7 bci=8
7        }

main[1]

第二步 查看pid

新开一个窗口,输入jps,查看java进程的pid:

E:\IDEA_daiMa\sjjg\hsdb\java>jps
19956 TTY
11944 Test2
3704 Jps

可以查看到Test2的pid是11944。

第三步 使用hsdb

jdk9之前使用此命令:

java -cp .;%JAVA_HOME%/lib/sa-jdi.jar sun.jvm.hotspot.HSDB  

jdk9及jdk9之后使用此命令:

jhsdb hsdb

打开就是这个界面了:

image.png

使用pid连接

image.png

然后映入眼帘的是进程里的线程:

image.png

把鼠标移到窗口右上角第二个图标上显示的是当前线程栈帧的数据:

image.png

打开看一下:

image.png

左起第一列是内存地址。

第二列是该地址上存的数据,由这两行的数据可以看出jvm应该是按字节编址,左边的地址每次跳过8个,而数据也是存储了8个字节。

第三列是对数据的注释,竖线表示范围,横线或斜线连接范围与注释文字。

好hsdb先介绍到这里,接下来开始正式分析。

第四步 开始分析

打开命令行:

image.png

按下回车键就可以输入命令了。

可以先输入一下help查看一下所有的命令。

然后输入universe查看堆内存的地址:

image.png

由上面的图片可以分析出,所有的对象都在eden区,其它的区域暂时没有被使用到。

然后,可以使用scanoops命令查看Tes的三个对象具体在哪

image.png

scanoops接受两个必选参数和一个可选参数:必选参数是要扫描的地址范围,一个是起始地址一个是结束地址;可选参数用于指定要扫描什么类型的对象实例。实际扫描的时候会扫出指定的类型及其派生类的实例。

可以看出这个工具还是比较np的,直接帮咱们把这三个对象的具体位置给找了出来。

然后可以用findpc(jdk1.8使用whereis)查看具体的信息。

image.png

还可以查看对象存储具体内容:

image.png

可以看出Tes的实例要16个字节。包括对象头和执行class的指针。

如果还想看更裸的数据,jdk8的同学可以直接用mem命令查看,jdk8之后没有了这个命令,但是可以用这个窗口查看。

image.png

image.png

分析一下内容的含义:

0x00000000fa49a710:  _mark:                        0x0000000000000001   
0x00000000fa49a718:  _metadata._compressed_klass:  0x00c00c10  
0x00000000fa49a71c:  (padding):                    0x00000000  

一个Tes的实例包含2个给VM用的隐含字段作为对象头,和0个Java字段。
对象头的第一个字段是mark word,记录该对象的GC状态、同步状态、identity hash code之类的多种信息。
对象头的第二个字段是个类型信息指针,klass pointer。这里因为默认开启了压缩指针,所以本来应该是64位的指针存在了32位字段里。
最后还有4个字节是为了满足对齐需求而做的填充(padding)。

于是我们要找t1、t2、t3这三个变量,等同于找出存有指向上述3个Tes实例的地址的存储位置。

不嫌麻烦的话手工扫描内存去找也能找到,不过幸好HSDB内建了revptrs命令,可以找出“反向指针”——如果a变量引用着b对象,那么从b对象出发去找a变量就是找一个“反向指针”。

image.png

ok 我们发现了一个class对象引用到了咱们的t1。

接下来可以用,findpc和inspect命令查看对象具体的存储位置和存储的内容:

image.png

可以看到,这个Class对象里存着Test类的静态变量t1,指向着第一个Tes实例。

成功找到t1了!这个有点特别,本来JVM规范里也没明确规定静态变量要存在哪里,通常认为它应该在概念中的“方法区”里;但现在在JDK7的HotSpot VM里它实质上也被放在Java heap里了。可以把这种特例看作是HotSpot VM把方法区的一部分数据也放在Java heap里了。
在JDK7之前的Oracle/Sun JDK里的HotSpot VM把静态变量存在InstanceKlass末尾,存在PermGen里。那个时候的PermGen更接近于完整的方法区一些。

再接再厉,用revptrs看看第二个Test2实例有谁引用:

image.png

可以看到这个Test实例里有个成员字段t2,指向了第二个Tes实例。

那么赶紧试试用revptrs命令看第三个Tes实例:

image.png

栈帧中的变量没有查到。

然后 我们可以去看一下刚开始出现的这张图:

从Test.f()的栈帧中我们可以看到t3变量就在locals[1]的位置上。t3变量也找到了!大功告成!


标题:hsdb分析运行时数据
作者:哇哇哇哇
地址:https://wuxiangshi.vip/articles/2022/04/17/1650168211285.html