从一道面试题初探JVM之内存布局、对象占用内存大小
测试环境
机器
CentOS Linux release 7.6.1810 (Core) x64
openjdk version “1.8.0-internal-debug”
代码
问题:
运行第三行代码之后,会在JVM堆中创建几个对象?
每个对象多少个字节?
对象的内存布局如何?
JVM其他内存区域,比如字符串常量池、线程栈有什么变化?
不开启指针压缩测试
即添加JVM启动参数:-XX:-UseCompressedOops
猜测
此时我们这段代码的JVM内存布局应该是这样的
"99"这个字符串常量,(假设)不存在于字符串常量池中,会去Java堆中创建String对象,创建String的过程中需要先创建value对象(char[]),创建完成String对象之后,会在字符串常量池(底层实现是StringTable,一个哈希表结构)中缓存一份,然后把String对象的引用地址赋值给main线程栈的栈帧中的局部变量1(局部变量0为args参数)
验证
0x00007f26fd11cc80即s1对象内存的起始地址,这里拿出从该地址开始的80个字节的内存(JVM对象都是8字节对齐的):
hsdb> mem 0x00007f26fd11cc80 10
0x00007f26fd11cc80: 0x0000000000000001
0x00007f26fd11cc88: 0x00007f273c67ac70
0x00007f26fd11cc90: 0x00007f26fd11cca0
0x00007f26fd11cc98: 0x0000000000000000
0x00007f26fd11cca0: 0x0000000000000001
0x00007f26fd11cca8: 0x00007f273c670210
0x00007f26fd11ccb0: 0x0000000000000002
0x00007f26fd11ccb8: 0x0000000000390039
0x00007f26fd11ccc0: 0xbaadbabebaadbabe
0x00007f26fd11ccc8: 0xbaadbabebaadbabe
分析如下:
s1对象 内存地址 | 值 | 含义 |
---|---|---|
0x00007f26fd11cc80 | 0x0000000000000001 | s1对象._mark,8字节,值为1代表无锁 |
0x00007f26fd11cc88 | 0x00007f273c67ac70 | s1对象._metadata._klass,指向String类元信息的指针,结合下面的当前VM的堆的内存布局,可以知道类元信息不是存储在JVM堆中 |
0x00007f26fd11cc90 | 0x00007f26fd11cca0 | 这个重要了,这个值是个内存地址,指向s1对象.value属性 |
接下来分析value对象,从地址0x00007f26fd11cca0开始:
value对象 内存地址 | 值 | 含义 |
---|---|---|
0x00007f26fd11cca0 | 0x0000000000000001 | value对象._mark,8字节,值为1代表无锁 |
0x00007f26fd11cca8 | 0x00007f273c670210 | value对象._metadata._klass,此时未开启指针压缩,为8字节 |
0x00007f26fd11ccb0 | 0x00000002 | value对象._max_length,表示数组对象的数组长度,4个字节 |
0x00007f26fd11ccb4 | 0x00000000 | value对象头填充?,4个字节,这样对象头是8*3=24个字节 |
0x00007f26fd11ccb8 | 0x00390039 | value对象实例数据,Java中每个char占用两个字节,0x0039的ascii码是‘9’ |
0x00007f26fd11ccbc | 0x00000000 | value对象实例数据部分的填充,这样实例数据正好是8个字节 |
这里用hsdb看下value对象的内存情况:
32个字节没错,但是没有标识出数组长度、填充的字节,可以用jol-core这个jar打印出内存布局进行互相验证(我这边暂时不做了)
这里看下当前VM的堆的内存布局
Heap Parameters:
ParallelScavengeHeap [ PSYoungGen [ eden = [0x00007f26fce00000,0x00007f26fd194310,0x00007f26feb00000] , from = [0x00007f26fef80000,0x00007f26fef80000,0x00007f26ff400000] , to = [0x00007f26feb00000,0x00007f26feb00000,0x00007f26fef80000] ] PSOldGen [ [0x00007f26b1600000,0x00007f26b1600000,0x00007f26b6200000] ] ]
尝试源码分析
看len是2,怎么看name执行的内存地址存储的是不是"99"呢?
通过上图可以看到地址0x7ffff0009750存储的值是3735609
通过python把十进制3735609转换成二进制为:
>>> a=3735609
>>> print("%#x"%a);
0x390039
linux shell man ascii命令可以看到0x39对应字符9
再结合len=2,读取2*2个字节(Java中每个char占用2个字节),就是0x00390039,而0x0039对应的ascii码就是字符’9’
ps:通过以上JVM源码,我们也知道为什么Java中的char是2个字节,因为它对应的VM中的类型是jchar,而jchar又是
typedef unsigned short jchar;
那为什么short又是2个字节?C++的知识了。。。
默认开启指针压缩测试
几点感悟
1.所谓的Java对象的内存占用,其实指的是每个Java对象所对应的VM中的C++对象的内存占用,这个C++类一般是
参考
1.https://www.bilibili.com/video/BV14i4y1K7Zv?p=31
2.https://club.perfma.com/article/2261053