对象的基本结构

了解对象的结构对以后的学习有很大的作用,本文主要对64位虚拟机下的对象进行剖析。

对象在创建的时候都有一个对象头,里面包含了对象的基本信息,一个对象的结构如下:

对象的组成.png

数组的本质也是对象,他的组成和这个类似,如下:

数组的组成.png

在进行探讨之前,我们先来看看实例

先定义一个user类

public class User {
private String name;
private int id;
private int sex;
private boolean finish;
}

maven引入依赖

org.openjdk.jol
jol-core
0.9

执行代码

public class Header {
static User u = new User();
static User[] users = new User[10];
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(u).toPrintable());
System.out.println(ClassLayout.parseInstance(users).toPrintable());
}
}

运行结果

运行结果.png

解析上面图片(图片当时编辑的时候没注意,有错误,懒得修改了,里面的对象头应该为MarkWord,前三行才叫对象头,数组还需要加上长度)

解析.png

首先解释一下上面的一些术语,在java的对象中,对象的大小都为8byte的倍数。

alignment/padding gap

alignment 对齐,对齐都是向8byte对齐

padding 补齐,补齐是向4byte补齐,对象对齐的最小粒度为4byte。

从上图看user对象和数组中前两行为MarkWord,第三行为KlassPointer,这三行加起来为对象头,后面的对象属性中因为int和String为4Byte,所以分配了4Byte,而boolean的内存占用为1Byte,根据java中必须为8的倍数,所以先给boolean补齐为4byte,最后在判断大小是不是8的倍数,如果不是就会进行填充,例如上面的loss due to the next object aligment。从这个对象结构看,我们浪费了7byte的空间。而User数组多了一个长度用来存储数组的长度,后面的数组数据用来存储User的对象指针,通过上面我们就可以知道为什么数组的最大长度为2^31次方了,因为数组长度分配了4byte的空间来储存。

通过上面可能又有疑问了,User类的属性顺序明明为name、id、sex、finish,为什么打印出来的不一样? 这是因为jvm在Heap中给对象布局的时候,会对field进行重排序,用来节省空间,上面的User类只有finish不是4byte,所以我们看不出效果,当我们有的Boolean、byte、char等类型不为4byte的属性时,如果不进行重排序的话我们会浪费很多的空间。

通过上面这句话就知道为什么会有alignment/padding了。

对象头

64位对象头由Mark Word、klass pointer两部分组成,如果对象是数组,则还要加上数组长度,即三部分组成。

Mark Word和klass pointer以及length都由64位8个字节组成。由于64位jvm默认使用选项 +UseCompressedOops 开启指针压缩,所以我们看到klass pointer和length只有32位。数组长度和对象指针这里不做描述,懂得都懂,重点讲一下Mark Word

|--------------------------------------------------------------------------------------------------------------|
| Object Header (128 bits) |
|--------------------------------------------------------------------------------------------------------------|
| Mark Word (64 bits) | Klass Word (64 bits) |
|--------------------------------------------------------------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁
|----------------------------------------------------------------------|--------|------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁
|----------------------------------------------------------------------|--------|------------------------------|
| | lock:2 | OOP to metadata object | GC
|--------------------------------------------------------------------------------------------------------------|

以上是Java对象处于5种不同状态时,Mark Word中64个位的表现形式,上面每一行代表对象处于某种状态时的样子。

lock: 锁状态标记位,该标记的值不同,整个mark word表示的含义不同。

biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁

锁.png

age:Java GC标记位对象年龄,因为用四个bit来储存,所以范围为0-15,因此对象经过了15次垃圾回收后如果还存在,则肯定会移动到老年代中。

identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用System.identityHashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。

thread:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。

epoch:偏向时间戳。

ptr_to_lock_record:指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:指向线程Monitor的指针。

具体解析

在分析上表对象结构的时候需要先明白System.identityHashCode()和hashCode()的区别。他们都是生成对象的hashCode。看下面代码

String str = new String("test");
String str2 = new String("test");
System.out.println(str.hashCode());
System.out.println(str2.hashCode());
System.out.println(System.identityHashCode(str));
System.out.println(System.identityHashCode(str2));

输出

结果.png

可以看出hashCode()和System.identityHashCode()得到的值不一样并且str和str2的hashCode()得到的hash是一样的,通过后面那种方法得到的是不一样的。

System.identityHashCode()是通过地址来生成hashCode,因为str和str2都是new出来的,所以他们通过其得到的不一样,而String类重写了hashCode,让他通过内容来生成hashCode,所以str和str2通过hashCode()得到的hashCode是一样的。而System.identityHashCode()和HashCode()得到的结果不一样的原因同上,System.identityHashCode()通过地址,String中的HashCode()通过内容。

然后我们就来看看无锁状态下的对象头储存的信息。

public class Header {
static User u = new User();
static User[] users = new User[10];
public static void main(String[] args) {
System.identityHashCode(u);
System.out.println(ClassLayout.parseInstance(u).toPrintable());
System.out.println(Integer.toBinaryString(System.identityHashCode(u)));
}
}

结果.png

无锁状态的对象信息在上面已经标注出,hashCode可以进行比对,具体的GC信息可以通过执行System.gc()验证。

各种锁状态下的对象头的具体分析等以后深入学习了并发后再回来补充。