使用JHSDB分析Java对象的存储

本文通过《深入理解JVM虚拟机》中的案例,详细解析了staticObj、instanceObj和localObject在JVM中的存储位置,涉及方法区、Java堆和栈帧。实验和JHSDB工具展示了变量存储在类加载器、对象实例和栈内存的不同情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文来自《深入理解JVM虚拟机》的 4.3.1 JHSDB:基于服务性代理的调试工具。

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

staticObj 随着Test的类型信息存放在方法区,instanceObj 随着Test的对象实例存放在Java堆,localObject 则是存放在foo()方法栈帧的局部变量表中。

public class JHSDB_TestCase {
    static class Test {
        static ObjectHolder staticObj = new ObjectHolder();
        ObjectHolder instanceObj = new ObjectHolder();
        private static Integer MAX_COUNT = 100;
        private String name;

        void foo() {
            ObjectHolder localObj = new ObjectHolder();
            System.out.println("done"); // 这里设一个断点
        }
    }

    private static class ObjectHolder {
        private String holderName;
    }

    public static void main(String[] args) {
        Test test = new JHSDB_TestCase.Test();
        test.foo();
    }
}

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

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

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

jhsdb hsdb --pid 21588

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

首先点击菜单中的 Tools->Heap Parameters,结果如图所示,因为在运行参数中指定了使用的是Serial收集器,图中我们看到了典型的Serial的分代内存布局,Heap Parameters窗口中清楚列出了新生代的Eden、S1、S2和老年代的容量(单位为字节)以及它们的虚拟内存地址起止范围。
Heap Parameters上面的效果与在 Windows->Console 中输入 universe 命令是等价的,JHSDB 的图形界面中所有操作都可以通过命令行完成:
Command Line如果读者实践时不指定收集器,即使用JDK默认的G1的话,得到的信息应该类似如下所示:
在这里插入图片描述打开 Windows->Console 窗口,使用 scanoops 命令在Java堆的新生代(从Eden起始地址到To Survivor结束地址)范围内查找ObjectHolder的实例,但我测试的时无法查到,不知为何???因此使用下面的方法查看。

打开 Tools->Object Histogram 窗口,找到相应的对象:
在这里插入图片描述
双击对应的条目,显示出下面三个实例的地址,而且它们的地址都落到了 Eden 的范围之内,算是顺带验证了一般情况下新对象在 Eden 中创建的分配规则。
在这里插入图片描述再点击 Inspector 按钮查看这三个地址中存放的对象,结果如图所示。
在这里插入图片描述Inspector为我们展示了对象头和指向对象元数据的指针,里面包括了Java类型的名字、继承关系、实现接口关系,字段信息、方法信息、运行时常量池的指针、内嵌的虚方法表(vtable)以及接口方法表(itable)等。还可以看到我们在ObjectHolder中定义的 holderName 字段。

接下来要根据堆中对象实例地址找出引用它们的指针,原本 JHSDB 的 Tools 菜单中有 Compute Reverse Ptrs 来完成这个功能,但在笔者的运行环境中一点击它就出现Swing的界面异常,看后台日志是报了个空指针(我也有这个报错),这个问题只是界面层的异常,跟虚拟机关系不大,所以笔者没有继续去深究,改为使用命令来做也很简单,先拿第一个对象来试试看:
在这里插入图片描述果然找到了一个引用该对象的地方,是在一个java.lang.Class的实例里,并且给出了这个实例的地址,通过 Inspector 查看该对象实例,可以清楚看到这确实是一个java.lang.Class类型的对象实例,里面有一个名为 staticObj 的实例字段,还可以看到定义的 MAX_COUNT,如图所示:
在这里插入图片描述
从《Java虚拟机规范》所定义的概念模型来看,所有Class相关的信息都应该存放在方法区之中,但方法区该如何实现,《Java虚拟机规范》并未做出规定,这就成了一件允许不同虚拟机自己灵活把握的事情。JDK 7及其以后版本的HotSpot虚拟机选择把静态变量与类型在Java语言一端的映射Class对象存放在一起,存储于Java堆之中。从我们的实验中也明确验证了这一点。

接下来继续查找第二个对象实例:
在这里插入图片描述这次找到一个类型为 JHSDB_TestCase$Test 的对象实例,在Inspector中该对象实例显示如图所示,以及定义的 name 字段:
在这里插入图片描述这个结果完全符合我们的预期,第二个ObjectHolder的指针是在Java堆中 JHSDB_TestCase$Test 对象的instanceObj字段上。

但是我们采用相同方法查找第三个ObjectHolder实例时,JHSDB返回了一个null,表示未查找到任何结果:
在这里插入图片描述看来 revptrs 命令并不支持查找栈上的指针引用,不过没有关系,得益于我们测试代码足够简洁,人工也可以来完成这件事情。在Java Thread窗口选中main线程后点击Stack Memory按钮查看该线程的栈内存,如图所示。
在这里插入图片描述
在这里插入图片描述
JHSDB在旁边已经自动生成注释,说明这里确实是引用了一个来自新生代的 JHSDB_TestCase$ObjectHolder 对象。

至此,本次实验中三个对象均已找到,并成功追溯到引用它们的地方,也就实践验证了开篇中提出的这些对象的引用是存储在什么地方的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值