占小狼
转载请注明原创出处,谢谢!
本文将借助HSDB工具分析HotSpot VM的运行时数据,运行的java环境为jdk1.8。
class Test {
static String version = "1.0";
String name;
int id;
Test(String name, int id) {
this.name = name;
this.id = id;
}
static void fn() {}
void fn2(){}
}
public class Main {
static Test t1 = new Test("java", 1);
private Test t2 = new Test("java", 2);
public void fn() {
Test t3 = new Test("java", 3);
}
public static void main(String[] args) {
new Main().fn();
}
}
运行上述代码,会在Java堆中生成3个Test对象,变量t1,t2,t3分别存储在方法区、实例字段和局部变量表中,那么Test对象的内存是如何布局的呢?
在查看运行时数据之前,需要让程序刚好执行完new Main().fn();
并暂停,平时可能习惯了在Eclipse、IntelliJ IDEA、NetBeans等Java IDE里使用Java层调试器,但为了减少对外部工具的依赖,本文将使用Oracle JDK自带的jdb工具来完成此任务。
jdb使用步骤如下:
1、jdb -XX:+UseSerialGC -Xmx10m
命令启动jdb;
2、stop in Main.fn
命令指定在方法入口设置断点;
3、run Main
命令指令主类,启动java程序;
4、next
命令可以向前执行一步;
采用jps命令查看目前调试java程序的PID
采用命令java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
启动HSDB工具,并连接到目标进程上,注意:Windows上Oracle JDK7才可以用HSDB。
连接上之后
默认窗口是Java Threads,显示当前进程的线程列表,双击线程打开一个Oop Inspector窗口,显示该线程在HotSpot VM的对象。
在菜单里选择Windows -> Console,打开HSDB里的控制台,用命令查看更多信息。
1、命令universe
查看GC堆的大小、地址范围和使用情况;
hsdb> universe
Heap Parameters:
Gen 0: eden [0x00000000ff600000,0x00000000ff6d50a0,0x00000000ff8b0000) space capacity = 2818048, 30.964980014534884 used
from [0x00000000ff8b0000,0x00000000ff8b0000,0x00000000ff900000) space capacity = 327680, 0.0 used
to [0x00000000ff900000,0x00000000ff900000,0x00000000ff950000) space capacity = 327680, 0.0 usedInvocations: 0
Gen 1: old [0x00000000ff950000,0x00000000ff950000,0x0000000100000000) space capacity = 7012352, 0.0 usedInvocations: 0
可以发现HotSpot在1.8的Java堆中,已经去除了Perm gen区,由youyoung gen和old gen组成。
2、命令scanoops
查看指定类型的实例对象,接受两个必选参数和一个可选参数:必选参数是要扫描的地址范围,一个是起始地址一个是结束地址;可选参数用于指定要扫描什么类型的实例对象;
hsdb> scanoops 0x00000000ff600000 0x0000000100000000 Test
0x00000000ff6caf08 Test
0x00000000ff6caf40 Test
0x00000000ff6caf58 Test
通过执行结果可以看出,Java堆上的确有3个Test实例对象,对象的开始地址分别为0x00000000ff6caf08、0x00000000ff6caf40和0x00000000ff6caf58。
3、命令whatis
可以查看指定内存地址所在的区域;
hsdb> whatis 0x00000000ff6caf08
Address 0x00000000ff6caf08: In thread-local allocation buffer for thread "main" (1)
[0x00000000ff6c7448,0x00000000ff6caf70,0x00000000ff6d5090,{0x00000000ff6d50a0})
hsdb> whatis 0x00000000ff6caf40
Address 0x00000000ff6caf40: In thread-local allocation buffer for thread "main" (1)
[0x00000000ff6c7448,0x00000000ff6caf70,0x00000000ff6d5090,{0x00000000ff6d50a0})
hsdb> whatis 0x00000000ff6caf58
Address 0x00000000ff6caf58: In thread-local allocation buffer for thread "main" (1)
[0x00000000ff6c7448,0x00000000ff6caf70,0x00000000ff6d5090,{0x00000000ff6d50a0})
上述结果可以发现3个Test实例对象都在分配给main线程的thread-local allocation buffer (TLAB)中。
4、命令inspect
可以查看对象的内容;
hsdb> inspect 0x00000000ff6caf08
instance of Oop for Test @ 0x00000000ff6caf08 @ 0x00000000ff6caf08 (size = 24)
_mark: 1
_metadata._compressed_klass: InstanceKlass for Test
name: "java" @ 0x00000000ff6644a8 Oop for java/lang/String @ 0x00000000ff6644a8
id: 1
instance of Oop for Test:表明该地址代表的对象是Test类的实例
_mark:对象头的第一个字段,记录该对象的状态
_metadata._compressed_klass:指向描述Test类信息的对象
name:实例的字段
id:实例的对象
可以发现_metadata._compressed_klass并没有显示内存地址,是因为该对象在java8中并非在堆中进行分配。
5、命令mem
可以看更直接的数据,接受的两个参数,起始地址和以字宽为单位的“长度”;
hsdb> mem 0x00000000ff6caf08 4
0x00000000ff6caf08: 0x0000000000000001 //_mark
0x00000000ff6caf10: 0x0000000111c10228 //_metadata._compressed_klass
0x00000000ff6caf18: 0x00000000ff6644a8 //name
0x00000000ff6caf20: 0x0000000000000001 //id
可以发现_metadata._compressed_klass所指向的地址0x0000000111c10228已经超出了Java堆的最大地址,所以通过执行inspect 0x0000000111c10228
并不会返回对象内容。
不过_metadata._compressed_klass的内容,可以通过在Inspector窗口中输入Test实例对象的开始地址进行查看。
- InstanceKlass是类的描述对象,存储着Java类型名称、继承关系、接口、字段信息、方法信息、虚方法表和接口方法表等数据,不过InstanceKlass是给VM内部使用的,并不直接暴露给用户;
- InstanceKlass中维护了一个字段_java_mirror,指向类的Class对象,所以当使用
obj.getClass()
获取Class对象时,是通过obj -> _klass -> _java_mirror
的过程进行获取的; - 在jdk7之前,HotSpot把类的静态字段保存在InstanceKlass中;从jdk7开始,为了配合perm gem的移除工作,静态字段被移动到Class对象中,如Test类中的version变量,存放在_java_mirror所指向的Class对象中。
6、命令revptrs
可以找出反向指针(如果变量a指向对象b,那么可以从b对象出发找到变量a);
查看第一个Test实例
hsdb> revptrs 0x00000000ff6caf08
Computing reverse pointers...
Done.
null
Oop for java/lang/Class @ 0x00000000ff6c9928
这个变量在Class对象中,其实是Main类的Class对象,所以该变量为t1;
通过whatis命令查看该Class对象的分配位置
hsdb> whatis 0x00000000ff6c9928
Address 0x00000000ff6c9928: In thread-local allocation buffer for thread "main" (1)
[0x00000000ff6c7448,0x00000000ff6caf70,0x00000000ff6d5090,{0x00000000ff6d50a0})
这个Class对象也是在eden里,具体来说在main线程的TLAB中,这个Class对象如何引用到Test类的实例?
通过inspect
命令查看Class对象的内容
hsdb> inspect 0x00000000ff6c9928
instance of Oop for java/lang/Class @ 0x00000000ff6c9928 @ 0x00000000ff6c9928 (size = 104)
<>:
t1: Oop for Test @ 0x00000000ff6caf08 Oop for Test @ 0x00000000ff6caf08
可以发现Main类的Class对象中存储了字段t1指向Test类的实例,该实例的起始地址正好是0x00000000ff6caf08。
JVM规范中并没明确规定静态变量的存放位置,通常应该放在“方法区”中,不过在jdk7的HtoSpot实现中,静态变量被保存在了Java堆中;
前面也提到过,在JDK7之前的HotSpot实现中,静态变量被保存在InstanceKlass里,并放在PermGen中;
查看下一个Test实例
hsdb> revptrs 0x00000000ff6caf40
Computing reverse pointers...
Done.
Oop for Main @ 0x00000000ff6caf30
这个变量在Main类一个实例中,为t2;
通过inspect
命令查看Main实例的内容
hsdb> inspect 0x00000000ff6caf30
instance of Oop for Main @ 0x00000000ff6caf30 @ 0x00000000ff6caf30 (size = 16)
<>:
_mark: 1
_metadata._compressed_klass: InstanceKlass for Main
t2: Oop for Test @ 0x00000000ff6caf40 Oop for Test @ 0x00000000ff6caf40
在该实例中,的确存在字段t2指向起始地址为0x00000000ff6caf40的Test实例。
查看最后一个Test实例
revptrs 0x00000000ff6caf58
Computing reverse pointers...
Done.
null
结果null,说明没有找到...
排除了前面两个Test实例,说明这个实例对应的变量应该为t3,该变量t3被保存在Main.fn方法调用栈中。
选择main线程,并点击图示的按钮,打开Stack Memory窗口如下:
Stack Memory窗口中,包含Main.fn()
和Main.main()
方法调用对应的栈帧,其中红色框框中对应Main.fn()
的栈帧
第1列为内存地址,该地址指虚拟内存意义上的地址,而非物理地址;
第2列为该地址上的数据,以字宽为单位;
第3列是对数据的注释;
先看看栈帧的结构,一个栈帧从上到下,分别包含了操作数栈、栈帧信息和局部变量表。
通过inspect
查看局部变量表数据,局部变量表第0位置slot[0] = "this",指向 Main实例
hsdb> inspect 0x00000000ff6caf30
instance of Oop for Main @ 0x00000000ff6caf30 @ 0x00000000ff6caf30 (size = 16)
_mark: 1
_metadata._compressed_klass: InstanceKlass for Main
t2: Oop for Test @ 0x00000000ff6caf40 Oop for Test @ 0x00000000ff6caf40
局部变量表第1位置slot[1] = "t3",指向Test实例,该实例正好是最后一个Test实例
hsdb> inspect 0x00000000ff6caf58
instance of Oop for Test @ 0x00000000ff6caf58 @ 0x00000000ff6caf58 (size = 24)
_mark: 1
_metadata._compressed_klass: InstanceKlass for Test
name: "java" @ 0x00000000ff6644a8 Oop for java/lang/String @ 0x00000000ff6644a8
id: 3
参考
借HSDB来探索HotSpot VM的运行时数据
我是占小狼
坐标魔都,白天是上班族,晚上是知识的分享者
如果读完觉得有收获的话,欢迎点赞加关注