likes
comments
collection
share

【JVM系列】Java对象在JVM内存中的布局

作者站长头像
站长
· 阅读数 2

前言

众所周知,Java对象在Java虚拟机中大部分情况下会被分配到内存模型中的Java堆区(栈上分配除外),然而,Java对象在Java堆区是以什么样的格式存储的呢,即所谓的Java对象在内存中的布局是怎样的。本文就以上问题进行简单介绍说明Java对象在内存中的布局。

Java对象在内存中的布局

其实Java对象在堆内存中由三部分组成,分别是对象头实例数据对齐填充,如下图:

【JVM系列】Java对象在JVM内存中的布局

对象头

对象头顾名思义就像人的头一样,可以理解成是用来识别对象的个体身份信息的。想象下通过对象头应该能查询到这是什么类型的对象,对象的年龄,对象的唯一识别码等等。对象头是由MarkWord类型指针组成,数组对象还包含数组长度,如下:

【JVM系列】Java对象在JVM内存中的布局

MarkWord

MarkWord主要用于记录对象的一些重要信息,其中主要记录了HashCode分代年龄是否偏向锁锁标记位。其实的MarkWord的结构比较复杂,而且不是固定的,无锁状态、偏向锁、轻量级锁和重量级锁分别对应着不同结构的MarkWord。

类型指针

类型指针也可以叫做ClassMetadataAddress或KlassWord,主要用于对象的身份识别,指向元空间(JDK1.7之前的方法区)中对象的类元数据,即JVM通过这个指针确定该对象是哪个类的实例。

数组长度

这个字段只有当对象是数组对象时才存在,只存储数组长度。平时我们调用获取数组长度方法时就是直接中对象头的数组长度中取的值

实例数据

实例数据是指一个聚合量所有标量的总和,也就是是指当前对象属性成员数据以及父类属性成员数据。实例数据中存储着当类的基本类型成员变量和引用类型成员变量,如果是基本类型变量,直接存变量的值;如果如果是引用类型,那么直接存储指针

对齐填充

对齐填充主要是为了对象的大小要符合《Java虚拟机规范》的规定,必须是8的倍数,因此,对齐填充起到个补充作用,其本身是没有任何意义的。例如:一个对象的对象头+实例数据大小总和为12bytes,那么此时就会出现4bytes的对齐填充,JVM为对象补齐成8的整数倍:16bytes。

《虚拟机规范》中规定了:为了方便内存的单元读取、寻址、分配,Java对象的总大小必须要为8的整数倍

占用内存大小分析

因为不同位数的虚拟机(32/64位)采用不同大小的字宽存储对象头,而且不同位数的虚拟机下字宽的大小也是不同的(32位虚拟机中一个字宽的大小为4byte,64位虚拟机下一个字宽大小为8byte)。因此这里要分别说明。

32位虚拟机

32位虚拟机中采取2个字宽存储对象头,如果是数组则额外增加1个字宽,即4byte大小的MarkWord,4byte大小的类型指针,数组的话加上4byte的数组长度(哈哈!这里可以看出来数组的最大支持长度是多少了),非数组对象对象头占8个字节,数组对象对象头占12个字节。 实例数据,如果是基本类型数据,按照基本类型数据大小计算,如果是引用类型,不用计算引用指向的对象的成员变量大小,只计算引用大小占用一字宽为4byte。

【JVM系列】Java对象在JVM内存中的布局

64位虚拟机

64位虚拟机采取两个半字宽+半字宽对齐数据存储对象头,即8byte大小的MarkWord,8byte大小的类型指针,再加上4byte大小数组长度,非数组对象对象头占16个字节,数组对象对象头占20个字节。如果是开启指针压缩的情况下,8byte大小的类型指针压缩成了4byte,那么非数组对象对象头占12个字节,数组对象对象头占16个字节。 实例数据的计算方式和32位机一样,只是引用类型的指针未开启指针压缩占用8byte大小,开启则占用4byte。

指针压缩属于JVM的一种优化思想,一方面可以节省很大的内存开支,第二方面也可以方便JVM跳跃寻址*,*在64bit的虚拟机中为了提升内存的利用率,所以出现了指针压缩这一技术,指针压缩的技术会将Java程序中的所有引用指针(类型指针、堆引用指针、栈帧内变量引用指针等)都会压缩一半,而在Java中一个指针的大小是占一个字宽单位的,在64bit的虚拟机中一个字宽的大小为64bit,所以也就意味着在64位的虚拟机中,指针会从原本的64bit压缩为32bit的大小,而指针压缩这一技术在JDK1.7之后是默认开启的。

【JVM系列】Java对象在JVM内存中的布局 这里用64位虚拟机举例:

class Student{
    // 4 byte
    int age;
    // 8 byte
    double height;
    // 8 byte
    Teacher teacher;
}

class Teacher{
    // 4 byte
    int age;
    // 8 byte
    double height;
}

void test(){
    Teacher t = new Teacher();
    Student s = new Student();
    s.age =  15;
    s.height = 145.98;
    s.teacher = t;
}

计算出上面的Student对象在堆区占用了多少内存。

未开启指针压缩的情况下

Student对象大小 = 8 byte MarkWord + 8 byte类型指针 + 4 byte int + 8 byte double + 8 byte teacher指针 + 4 byte 对齐填充 = 40 byte

开启指针压缩的情况下:

Student对象大小 = 8 byte MarkWord + 4 byte类型指针 + 4 byte int + 8 byte double + 4 byte teacher指针 + 4 byte 对齐填充 = 32 byte