Java虚拟机(二)-HotSpot中Java堆的对象分配、布局和访问

在介绍了Java虚拟机的运行时数据区域后,我们对Java虚拟机在运行中放了什么有了一些了解,现在我们要讨论下Java中的对象是如何创建、布局和访问的。

对象的创建

当我们编写Java代码的时候,一个new指令就直接完成了对象的创建,但是这时候Java虚拟机具体是怎么创建一个正常的对象的呢?

  1. 检查:当Java字节码遇到new指令的时候,首先先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,就要先执行相应的类加载过程。
  2. 内存分配:在类加载完成后,对象所需要的内存便可以确定,在Java堆中划分出一块确定大小的内存即可。

    注意:需要特别说明下这里的内存分配,Java堆中的内存分配有两种情况:

    1. 内存碰撞:如果内存是规整的,那么虚拟机只需要维护一个指针在分界点区分已使用和未使用内存区域即可,当分配内存的时候只需要将指针右移。

    2. 空闲列表:如果内存不规整,虚拟机就只能维护一个列表,来记录哪些内存是可用的,在分配的时候通过列表来寻找空闲内存,分配完成后再修改该表。 1

    注意:由于对象创建在虚拟机中非常频繁,并且是并发操作,因此不得不考虑线程安全问题,对此也有两种方案:

    1. 对分配空间内存的工作进行同步处理,即采用CAS配上失败重试的方式保证更新操作的原子性。

    2. 把内存分配的工作按照线程划分在不同的空间之中进行。即每个线程都先分配一小块内存(本地线程分配缓冲),那个线程需要内存,就直接在对应线程的本地缓冲区中分配。当本地缓冲区分配完了,就需要再分配新的缓冲区,只有这时候才进行同步锁定。

  3. 初始化:将申请到的内存空间都初始化为零值。
  4. 对对象进行必要的设置:例如这个对象是哪个类的实例?对象的哈希码?如何找到类的元数据信息?这些信息存放在信息头当中,后面会详细介绍。
  5. 构造函数:其实上面几步对于虚拟机来说已经初始化完成了,但是对于对象本身还需要执行构造函数,也就是Class文件中的<init>()方法。

对象的内存布局

在虚拟机中对象的内存存放在Java堆中,可以将其划分为三个部分:对象头、实例数据、对齐填充。我们娓娓道来:

对象头

HotSpot中的对象头包括两类信息:

  1. 存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。这部份信息被称之为Mark Word,其长度和虚拟机的位数相同,即32位虚拟机长度就为32位,64位虚拟机就是64位;需要特殊说明的是,由于空间有限,该部分信息被设计成了动态定义的数据结构。
  2. 类型指针,就是对象指向其类型元数据的指针,Java虚拟机通过该指针来确定对象的类型。当然,不是所有的虚拟机必须在对象上保存类型指针,这个后面会说明。如果是数组的话,在对象头还会有一块用于记录数组长度的数据。

实例数据

这是对象真正存储的有效信息,也就是我们在类定义的各种类型的字段内容。
这部份内容的存储顺序受虚拟机分配策略参数(FieldsAllocationStyle参数)和字段在Java源码中定义的顺序影响。默认的顺序是longs/doubles、 ints、 shorts/chars、 bytes/booleans、 oops(OrdinaryObject Pointers, OOPs),也就是相同大小的数据总是会被分配到一块。

对齐填充

这部份内容可有可无,就是普通的占位符,没有特殊的意义。 HotSpot的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即任何对象的大小都必须是8字节的整数倍(对象头部分已经被设计为了8字节的整数倍),所以如果实例数据部分不是8的倍数,就需要该部分来补全。

对象的访问定位

在介绍Java内存区域的时候,我们在虚拟机栈的局部变量表里提到了reference类型,也就是对象引用。对对象的访问就通过reference类型。
由于《Java虚拟机规范》中只说明了它是一个指向对象的引用,但是没有具体实现,所以出现了两种不同的访问策略:句柄直接指针

句柄

通过句柄访问对象 如上图,首先需要额外在Java堆中分配一块句柄池,reference中存放的就是句柄地址,句柄中存放的是对象实例数据和类型数据的地址信息。

直接指针

通过直接指针访问对象
如上图,使用直接指针的时候,reference字节指向对象的实例信息,但是需要考虑如何放置访问类型数据的相关信息。如果只是访问对象,就减少了一次访问次数。

两种访问方式的对比

  1. 句柄:使用句柄的好处是,reference中存储的是稳定的句柄地址,在对象被移动的时候2 只需要改变句柄中的实例数据指针,而reference不需要修改
  2. 直接指针:最大的好处就是速度快,相较于句柄,直接指针减少了一次指针定位的时间开销,在Java语言中对对象的访问十分普遍,因此可以节省不少的时间。3

  1. 这里需要注意,使用什么分配方式是由内存是否规整决定的,而内存是否规整是由虚拟机采用的垃圾回收机制是否带有空间压缩整理所决定的,如果使用Serial、ParNew等带有压缩整理的收集器,就会使用指针碰撞,简单且高效;如果使用CMS这种基于清除算法的收集器时,理论上就只能采用空闲列表这种较为复杂的方式来分配内存

  2. 在垃圾回收机制中,对象的移动十分普遍。

  3. HotSpot就使用直接访问,当然句柄访问也很普遍。