概述:
平常我们都在使用对象,现在从底层角度来分析下java对象的内存布局,以及对象布局各部分含义。
Java对象内存布局构成:
对象头(Header):用于存储对象自身运行时数据,包括哈希值(hashcode)、类型、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
实际数据(Instance Data):用于存放类的数据信息,父类的信息,对象字段属性信息
对齐填充(Padding):为了字节对齐,填充的数据
内存布局如图所示:
我们可以使用依赖库来打印对象在内存中的布局信息:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.14</version>
</dependency>
这个库的常用方法:
ClassLayout.parseInstance(stu).toPrintable();//获取对象内部信息
举例:
public static void main(String[] args) {
Student stu = new Student(5, "eric");
System.out.println(ClassLayout.parseInstance(stu).toPrintable());
}
static class Student{
private int id;
private String name;
public Student(){
}
public Student(int id, String name){
this.id = id;
this.name = name;
}
public int getId() {return id;}
public void setId(int id) {this.id = id;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
}
结果输出:
上面例子展现的对象内存信息是 普通对象的,如果改成数组对象,则对象头多了一个Length Field,具体信息如下:
Student[] stuArr = new Student[10];
System.out.println(ClassLayout.parseInstance(stuArr).toPrintable());
对象头详细:
对象头主要有Mark Word,Plass Pointer,其中数组对象还包括Length Field。
Mark Word:
用于存储对象自身的运行时数据,如哈希码(hashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。占用空间大小根据JVM决定,为JVM的一个字大小,也就是32位JVM中Mark Word占用4个字节,64位JVM中占用8个字节64位。
为了节省空间,Mark Word是以非固定的数据结构来存储。具体数据结构如下:
32bit JVM:
64bit JVM:
各参数介绍:
锁标志位(lock):区分锁状态,这个参数总共占2bit,可以表示四种状态,但是上面图中,锁状态有五种,可以看出,无锁态 和 偏向锁都用 01 表示。那怎么区分无锁态跟偏向锁?这时引入 是否偏向锁 参数。
是否偏向锁(biased_lock):是否偏向锁,这个参数占 1bit,0表示 不是偏向锁,1表示 是偏向锁
分代年龄(age):表示java对象被GC的次数,每次GC的时候,如果对象在Survivor区复制一下,年龄增加1。当对象达到设定的阀值时,将会晋升到老年代。这个参数占 4bit,也就是最大值是 2^4 - 1 = 15。这是JVM参数XX:MaxTenuringThreshold选项最大值为15的原因。默认情况下并行GC的年龄阀值为15,并发GC的年龄阀值为6。
hashCode:对象的hashCode,使用方法System.identityHashCode()计算,采用延迟计算,计算后会把结果写到该对象头中。当对象被锁定时,该值会移动到Monitor中。
线程ID:在偏向模式中,当某个线程持有该对象,则该对象头的 线程ID位置存储的是这个线程的ID。这样在后面的操作中,就不需要再进行获取锁的动作
epoch:偏向锁时间戳,用于在CAS锁操作过程中,偏向性标识,表示更偏向哪个锁
ptr_to_lock_record:在轻量级锁的状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象头中设置指向锁记录的指针
ptr_to_heavyweight_monitor:在重量级锁的状态下,指向管程Montior的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor心管理等待的线程。在重量级锁定的情况下,JVM设置ptr_to_heavyweight_monitor指向Montior。
Klass Pointer:
这个字段存储对象的类元数据的指针。JVM通过这个指针来确定这个对象是哪个类的实例。占用JVM一个 字大小,即32bit JVM占4字节,64bit JVM占8字节
小结:
通过对象在内存中的布局分析,我们可以明白一些问题的底层解释。比如:
如何计算java对象在内存中占用空间大小:可以从java对象头、实体数据、填充数据三部分来计算
JVM如何获取对象的GC年龄:在对象头Mark Word有一个分代年龄(age)来记录对象的GC年龄
为啥最大GC15次后,对象就会被移动老年代:因为分代年龄(age)字段占用空间大小是4bit,也就是15,这个32bit还是64bit都是一样的
另外我们发现对象头中有很多锁状态相关的字段,这个主要跟synchronized锁有着,涉及锁升级,锁优化等。这个会在下一篇中介绍。