Java对象内存布局及对象头详解

概述:

平常我们都在使用对象,现在从底层角度来分析下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锁有着,涉及锁升级,锁优化等。这个会在下一篇中介绍。

 

 

 

 

 


版权声明:本文为herojuice原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。