Featured image of post Java 中的对象 -- 对象基本信息

Java 中的对象 -- 对象基本信息

讲述了 Java 对象创建流程以及对象组成,学习完本篇文章能够轻松计算出对象的大小

最近在看设计模式的时候,看到某篇文章下面有针对享元模式节省内存空间的分析,其通过分析对象创建的数量以及每个对象的大小进行了详细的分析,其中特别是针对单个对象大小的分析,其指出了很多我经常遇到却没有仔细关注的部分,想着抽空重新回顾一下对象的创建和组成这部分的知识,在回顾知识的基础上,弥补这部分的缺失!

问题

本篇文章,我们从以下几个问题入手,以解决问题的角度来理解这块的知识点。

  • 对象的创建流程是怎样的?
  • 一个对象有多大?
  • 对象的组成是怎样的?
  • 对象头里面有哪些数据?
  • 对象头和锁的关系?
  • hashCode是什么?hashCode和对象的关系?
  • 如何验证hashCode对偏向锁的影响?

接下来我们看看上面的问题该如何解决……

问题一:对象的创建流程

一个Java对象的创建过程主要包括类初始化类实例化两个阶段,其中类的初始化过程我已经在另一篇文章中有过介绍《深入JVM(一)》,所以我将在此基础上,进一步阐述一下一个Java对象的创建过程。

创建过程

Java的对象创建是一个非常复杂的过程,我们这里仅仅使用普通的Java对象为例进行说明,不包括数组和Class对象的创建过程

一、类加载检查

当虚拟机遇到一条new指令时:

  • 将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用
  • 检查这个符号引用代表的类是否已被加载、解析和初始化过
  • 如果没有,那必须先执行相应的类的加载过程,该过程以前文章中已经作过介绍。

二、为对象分配内存

对象分配的内存大小在类加载完成之后便会被完全确定(这个问题后面会介绍)

  • JVM将会从堆中加载一块确定大小的内存分配给该对象
  • 在内存分配的过程中,会涉及到内存的分配方式,按照Java堆是否规整分为两种方式:指针碰撞空闲列表
  1. 堆内存规整:已使用的内存放在一边,未使用的内存放在另外一边

  2. 堆内存不规整:已使用的内存和未使用的内存相互交错放置

指针碰撞
  • Java堆中内存绝对规整,内存分配将采用指针碰撞
  • 分配形式:已使用内存在一边,未使用内存在另一边,中间放一个作为分界点的指示器
  • 分配内存仅仅就是把指针指向空闲那边挪动一段与对象大小相等的距离

空闲列表
  • Java堆中内存不规整,内存分配将采用空闲列表
  • 分配形式:虚拟机维护着一个可用内存块的列表,在分配内存时,从列表中找到一块足够大的内存空间分配给对象实例,并更新列表上的记录
补充
  1. 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定,所以具体使用的分配方式和虚拟机使用的垃圾收集器有着重要关系

  2. 对象创建在虚拟机中是非常频繁的操作,即使仅仅修改一个指针所指向的位置,在并发情况下也会引起线程不安全,例如在给对象A分配内存的时候,指针还没有来得及修改,此时对象B又同时使用了原来的指针来分配内存,那么就会存在线程不安全问题

  3. 解决线程安全问题一般由两种方案:

    1.同步处理内存空间的分配

    虚拟机采用CAS配上失败重试的方法保证更新操作的原子性

    2.把内存分配的动作按照线程划分在不同空间之中进行

    每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲,TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,在需要进行同步锁定。

    通过设置-XX:+/-UseTLAB参数来设定

三、内存空间初始化

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头

注意⚠️

  1. 如果使用了TLAB,这一过程也可以提前到TLAB分配时进行;
  2. 该初始化动作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用。

四、针对对象进行必要设置

注意

这里的必要设置并非是针对步骤三中设置初始值的后续赋予我们程序代码中手动设置的值,而是针对对象头的一些操作,因为上面的内存空间初始化并没有包括对象头,所以这里肯定要对对象头进行处理。

这里设置的对象头信息比较多,例如设置这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等。

虚拟机角度的对象创建结束

到这里为止,从虚拟机角度创建对象就算结束了,但是从Java程序开发角度来说,对象创建才刚刚开始,毕竟我们代码中设置的那些值还没有被放到对象对应的字段中,即<init>方法还没有执行。

对象创建过程图解

问题二&三:一个对象有多大?组成是怎样的?

要想知道一个对象有多大,那么就需要知道这个对象的内存布局,知道对象中保存了哪些数据。

对象的内存布局

HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

对象头

对象头存储的信息包括两部分:

  • 对象自身运行时数据(Mark Word
    1. 存储内容包括:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳,这块数据大小和虚拟机有关,在的32位和64位虚拟机(未开启指针压缩)上大小分别为4byte8byte
    2. 为了虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,根据对象的状态不同,其数据存储的类型也不同,具体根据其中标志位的变化而存储不同数据。
  • 类型指针
    1. 对象只想它的类元数据的指针。
    2. 虚拟机通过这个指针来确定这个对象是哪个类的实例。
    3. 这部分数据在32位和64位虚拟机(未开启指针压缩)中大小分别为4byte8byte;如果开启指针压缩,在64位虚拟机下这部分大小为4byte

注意⚠️

因为虚拟机可以通过普通Java对象的元数据信息确定对象的大小,但是从数组的元数据中却无法确定数组的大小。所以如果对象是数组,那么在对象头中还必须有一块用于记录数组长度的数据,所以还需要4byte来记录数组长度。

虚拟机默认开启指针压缩,使用-XX:-UseCompressedOops可以关闭指针压缩,可以自己做测试来看看效果。如果指针压缩是打开的,一般情况下以下对象的指针会被压缩:

  • 所有对象的klass属性
  • 所有对象指针实例的属性
  • 所有对象指针数组的元素(objArray

实例数据

对象真正存储的有效信息,即程序代码中所定义的各种类型的字段内容,对象的方法并不直接保存在这里。

注意⚠️

这部分数据的存储顺序会受到虚拟机分配参数(FieldAllocationStyle)和字段在Java源码中定义顺序的影响。这样做能够提高字段的读取效率,将具有相同大小空间的存放到一起,同时按照大小排序,这样当我们需要获取某个类型的字段值时,直接通过计算得知该类型字段的起始位置,提高读取效率。在满足这样的条件下,父类定义的变量会保存到子类前面。

除此之外,通过设置-XX:+CompactFields (默认为true),可以将一些短类型的字段插入到类型空隙中,例如64bit 开启压缩指针,header占12个字节,剩下的4个字节就是空隙。

对齐填充

存储信息的占位符,没有特别含义,因为HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是对象大小必须是8字节的整数倍,所以当对象头加实例数据部分数据量之和没有对齐时,就可以通过对齐填充补全。

问题解答

现在我们可以来回答一个对象的大小了,首先在确定对象头的大小之后,这个对象的大小就主要由实例数据来确定了,当对象头加实例数据之和不是8字节的整数倍的话,就需要通过对齐填充来补全。

问题四:对象头里有哪些数据

当我们学习了问题二之后,问题三的基本组成我们应该是有了大概了解:对象头无非就是由**存储对象自身的运行时数据(Mark Word)指向它的元数据的类型指针(klass)**构成,类型指针这里暂时不进行解读,还是先来看看Mark Word里面究竟存储了哪些信息。

  • Mark Word主要存储一系列标志位信息,包括对象自身的哈希码、GC分代年龄、锁状态、持有锁的线程、偏向锁id、偏向时间戳等。

  • Mark Word32位和64位操作系统中,分别占32bit64bit。对象实际运行数据要大于这个值,对象头是与对象自身定义数据无关的独立存储空间,为了尽量多的存储数据,一般为非固定的数据结构。

  • 无锁状态下,在32位操作系统中,Mark Word32bit中使用25bit来存储对象的哈希码(hashCode),4bit存储GC分代年龄,2bit存储锁状态标志位,1bit 固定位 0

  • 在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储内容如下表所示。

  • HotSpot中关于Mark Word和锁的相关信息如下:

Mark Word中保存的信息可以看到一些信息

  • 对象的锁信息好像是保存到对象中的,而且一个对象的锁状态有且只有一种
  • 对象的哈希码也是保存在Mark Word中的,所以如果锁状态切换之后,代码中有调用对象哈希码的话那么锁状态会变成什么样子?
  • 一个对象一般存在四种锁状态,分别是无锁、偏向锁、轻量级锁、重量级锁,那么这些锁之间的具体切换条件是什么样子的?

由于篇幅原因,以上问题以及开篇的剩下问题将会在下篇文章进行解答

总结

本篇文章主要介绍了Java中对象的创建流程和对象的组成,从这篇文章中,可以知道以下一些信息:

  • 对象创建是在类初始化之后进行
  • 对象的内存分配主要有指针碰撞和空闲列表两种方式,具体使用哪种方式和jvm使用的垃圾收集器有关
  • 一个对象主要由对象头、实例数据和对齐填充三部分组成,其中对象头中的Mark Word中保存了对象自身运行时数据,实例数据则保存对象真正存储的有效信息,即程序代码中所定义的各种类型的字段内容
  • 对象头中的Mark Word和对象的锁存在着一些必然联系,随着其中标志位不同,锁状态会发生变化

参考

  • 《深入理解Java虚拟机》 - 周志明
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus