概述
我们知道关于Java对象的内存布局是由JVM所管理,所以我们就从Java虚拟机的官方文档出发,了解Java对象的内存布局。
对象内存布局
对象在内存中存储的布局可以分为3块区域:对象头、实例数据、对齐填充。
对象实例 |
---|
对象头 |
实例数据 |
对齐填充 |
对象头
这是OpenJDK里hotspot的文档关于对象头的描述:
大致意思是: 是每个gc管理的堆对象开头的公共结构。(每个oop指针都指向一个对象头。)包括堆对象的布局、类信息、GC状态、同步状态和标识哈希码的基本信息。由两个字组成。在数组中,它后面紧跟着一个长度字段。注意,Java对象和vm内部对象都有一个通用的对象头格式。
这里解释以下oop:
关于oop的描述:
大致意思: 是一个对象指针。特别是指向gc管理的堆的指针。(这个词很传统。一个“o”可以代表“普通”。实现为本机地址,而不是句柄。Oops可能被编译或解释的Java代码直接操作,因为GC知道Oops在这些代码中的活性和位置。Oops也可以由C/ c++代码的短周期直接操作,但是必须由这些代码在每个安全点的句柄中保存。
上面对象头中说一个对象头由两个字组成。我们来看看这两个字:
第一个字mark word
大致意思: 每个对象标头的第一个字。通常是一组位域,包括同步状态和标识哈希码。也可以是同步相关信息的指针(具有特征的低比特编码)。在GC期间,可能包含GC状态位。
意思是用于存储对象自身的运行时数据:(如哈希码、GC分代年龄、锁状态标志、线程持有锁、偏向线程ID、偏向时间戳等)。称“Mark Word”,(考虑到存储成本,是一个非固定的数据结构)。
我们来看看mark word里的具体信息:
下面这段代码是Open JDK里关于mark word的源码
这里面的注释,说明了32位机和64位机的mark word信息。
我们来看64位下对mark word的描述。
unused 25 :未使用25位
hash:31 :哈希占了31位
age:4 :表示老年代,新生代的占了4位。(这里需要说明一下,对象在Survivor区每“熬过”一次Minor GC ,对象的年龄就会+1,默认值为15,超过这个值就会晋升到老年代中。因为4位可以表示的最大值为15)。
biased_lock:1: 表示偏向锁占1位
lock:2 : 表示锁占2位
对象有5种状态:无锁、偏向锁、重量级锁、轻量级锁、GC。
HotSpot虚拟机对象头Mark Word
存储内容 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
第二个字 klass pointer
大致意思: 每个对象标头的第二个字。指向描述原始对象的布局和行为的另一个对象(元对象)。对于Java对象,“klass”包含一个c++风格的“vtable”。
意思是:另一部分是类型指针。(即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例)。
注意:对象头一共占128位,其中mark word占64位,klass pointer占64位
实例数据
是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。
对齐填充
下面结合对象信息来介绍。
这里我们可以通过编写一个类,把该类的对象信息输出验证以下:
那么问题来了,Java如何输出一个对象的信息呢?
OpenJDK里有一个jol(java object layout)的jar包。可以输出对象的信息。
一个普通类:
public class Simple {
private int state = 1; //只有一个成员
}
//通过main方法
private static Simple simple = new Simple();
public static void main(String[] args) {
//这句话便可以将对象信息输出
System.out.println(ClassLayout.parseInstance(simple).toPrintable());
}
来我们看输出结果:
com.aiun.test.Simple object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 47 c1 00 f8 (01000111 11000001 00000000 11111000) (-134168249)
12 4 int Simple.state 1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
我们可以清楚的看到,对象头输出了三块,每一块4字节,一共12字节。
但是这里我们只看到了对象头和实例数据,并没有看到对齐填充,怎么回事?
来,我们在给类加一个属性。
public class Simple {
private int state = 1;
private boolean flag = true;
}
//来,我们看输出结果
com.aiun.test.Simple object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 47 c1 00 f8 (01000111 11000001 00000000 11111000) (-134168249)
12 4 int Simple.state 1
16 1 boolean Simple.flag true
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
这次我们可以看出来,输出结果和上一次不一样了,多了 (loss due to the next object alignment) 意思是:由于下一次对象对齐而造成的损失
所以这另外的7个字节并不属于类信息,而是对齐填充造成的。
注意 对齐填充: 仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍。
那么为什么是8字节的整数倍,这样有什么优点呢?
- 系统要求
- 可以提高GC回收效率
对齐原理参考文档
接下来讨论一下mark word里面的信息:
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
就这个输出信息来讨论:
*上面说对象头一个占128位,为什么这里打印出来占12字节96位呢?
这是因为Java虚拟机默认开启了指针压缩。由于在64位CPU下, 指针的宽度是64位的, 而实际的heap区域远远用不到这么大的内存, 使用64bit来存对象引用会造成浪费, 所以应该压缩来节省资源。可以通过-XX:-UseCompressedOops参数来关闭指针压缩。
根据上面源码注释里面说,前25位是未使用,应该全为0,为什么对象信息输出后,第8位就为1?有1不是说明有数据吗?
因为存储是分大端、小段存储的。小端存储是反着来存储的(也就是高地址低字节),大端存储是顺着来存(高地址高字节)。所以这里是反正存储的。
就算反着来看,那接下来31位为hash,怎么全是0呢?
按理说Java的hashCode()确实存在,为什么这里没有呢?我们输出一下hashCode看看
21685669
0 4 (object header) 01 a5 e5 4a (00000001 10100101 11100101 01001010) (1256563969)
4 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
我们看到调用了hashCode()方法后,对象输出信息里就有hash值了,因为hash值是通过C++代码计算的。
我们将输出的结果转换为16进制:21685669 <—> 14AE5A5,可以看出输出的确实是hash码。
最后面的3个字节表示锁的状态,我们现在来加一下锁,看看
public static void main(String[] args) {
Simple simple = new Simple();
synchronized (simple) {
System.out.println(ClassLayout.parseInstance(simple).toPrintable());
}
}
输出
0 4 (object header) 70 f6 ad 02 (01110000 11110110 10101101 00000010) (44955248)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
最后三位为110,1表示偏向锁状态,10表示重量级锁(synchronized)。
总结:
我们可以看到出一个对象信息涉及到很多知识,对象状态、指针压缩、GC年龄、偏向延迟、批量撤销、锁膨胀可逆等相关知识。