深入理解JVM— Java对象的创建过程、对象内存布局、对象的引用方式详解

一、 对象的创建过程

(1) 判断类是否加载。检查常量池中是否可以定位到指定类的符号引用,并且检查这个符号引用所代表的类时候已经被加载、链接和初始化过。

  • 如果可以定位到符号引用,并且已经被加载过:进入第2步
  • 如果没法定位到符号引用或没有被加载过:执行相应的类加载过程

(2) 分配内存。(指针碰撞:Serial、ParNew/空闲列表:CMS)。
(3)初始化零值。为对象中的实例字段赋零值(不是给静态属性赋零值,静态属性在类加载的时候就初始化完成了,详情参考深入理解JVM—虚拟机类加载机制)。
(4) 设置对象头(Object Header)。如设置此对象是哪个类的实例、如何才能知道类的元数据信息、对象的hashcode、对象的GC分代年龄……。
(5)执行类的构造方法(站在JVM的角度是执行<init>方法)。初始化类的实例字段。

这5步执行后一个真正可用的对象才算完全产生出来。

二、 对象的内存布局

在HotSpot虚拟机中对象在内存中的存储布局可以分为:对象头、实例数据和对齐填充3部分。


图3.1 对象的内存布局
(1)对象头(Object Header)

对象头在HotSpot虚拟机中被分为了两部分,一部分官方称为Mark Word;另一部分是类型指针。如果对象是一个Java数组,那在对象头中还有一块用于记录数组长度的数据。

图3.2 对象头
  • 第一部分Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、对象分代年龄等信息。Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据自己的状态复用自己的存储空间
    在 32 位的 HotSpot 虚拟机中,如果对象处于未被锁定的状态下,那么 Mark Word 的 32bit 空间中的 25bit 用于存储对象哈希码,4bit 用于存储对象分代年龄,2bit 用于存储锁标志位,1bit 最后三位001标识对象处于无锁状态。如下表所示:

  • 第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。在不开启对象指针压缩的情况下是8字节。压缩后变为4字节,默认压缩。
    通过命令:java -XX:+PrintCommandLineFlags -version 查看classPointer是否开启压缩

InitialHeapSize=192266304 起始堆大小
MaxHeapSize=3076260864 最大堆大小
UseCompressedClassPointers 压缩指针,一般java是64位的操作系统,那么指针的长度即64位,即8字节,开启此命令后,classPointer压缩为4字节
UseCompressedOops 压缩普通对象指针 普通对象占用4字节

(2)实例数据(Instance Data)

       实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
       这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在 Java 源码中定义顺序的影响。

  • 原生类型的内存占用情况如下:
byte short char int long double float boolean
1字节 2字节 1字节 4字节 8字节 8字节 4字节 1字节
  • 引用类型的内存占用和系统位数以及启动参数UseCompressedOops有关

32位系统占4字节
64位系统,开启UseCompressedOops时(指针压缩),占用4字节,否则是8字节

(3)对齐填充(Padding)

       由于HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,即:Java对象的大小必须是8字节的整数倍,而对象头的大小正好是8字节的整数倍,所以当对象的实例数据没有对齐的时候,就需要对齐填充来补全。因此对齐填充并不是必须存在的,也没有特殊的含义,它仅仅起到了占位符的作用。

(4) 对象头内存占用分析

在 32 位JVM下,存放 Mark Word 空间的大小是4字节,Klass 指针的空间大小是 4 字节,因此头部就是 8 字节,如果是数组就需要再加 4 字节表示数组的长度,如下表所示:

在64位JVM未开启指针压缩下,头部的Mark Word和Klass Word都是8字节,这样一来头部就占用16字节,如下表所示:

在64位JVM开启指针压缩下,头部的ark Word没有变化还是8字节,但是Klass Word别压缩成了4字节,也就是64位系统下如果开启指针压缩对象头部最少为** 12 字节**,如下表所示:

针对对象头的各部分大小,还有一道经典的面试题:Object obj=new Object()这句代码需要占用多大的内存?
答:32位JVM下是12bytes(8+4),64位JVM下更据是否开启指针压缩分别是20\24bytes(16+4\8)。分析如下:

  • 32位操作系统下:一个对象由对象头(MarkWord+Klass pointer)、实例数据、对齐填充构成。在32位系统下,MarkWord 4bytes,Klass Poninter 4bytes,因此对象头就占有至少8bit,之后Object类中没有实例数据,因此这一段为0,之后按照虚拟机规范对齐填充为8bytes整数倍,最终Object对象占用8字节,然后引用Object o在桟中,32位系统中占用4bytes,因此总共占用12bytes

  • 64位操作系统下:MarkWord 8bytes,Klass pointer 4\8bytes,对象头总计12\16bytes,实例数据为0,之后再填充到8整数倍最终为16bytes,然后引用Object o在桟中,64位系统中占用4\8bytes,因此总共占用20\24bytes

三、 对象的访问定位

目前主流的对象访问方式有使用句柄直接指针两种方式。

(1)句柄访问

       在Java堆内存中划分一块内存专门用来作为句柄池,JVM桟的局部变量表的reference存储对象的句柄地址,而句柄中保存了对象实例数据与类型数据的具体地址,如下图所示:
enter image description here
       使用句柄方式最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

(2)直接指针访问

       此时reference直接存储对象在堆中的地址,不再需要句柄池,这样做最大的好处是提高了性能,因为它节省了一次指针定位的时间开销。然而这也是HotSpot VM所选择的对象访问方式。
直接指针访问

留言区

还能输入500个字符