JVM在运行时将内存区域划分为不同的数据区,各区域保存特定的数据类型。而类对象一般保存在Java堆中,类的创建、内存布局与访问都遵循一定的规范。

对象的创建

在Java程序中,对象的创建可以说随处可见,在语法层面上,对象的创建通过一个简单的new关键字即可实现,但在JVM中,对象的创建过程具有一套很繁琐的流程,其过程简要如下。

  1. 当JVM遇到一个new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。若没有,则执行类加载过程(类加载过程包括加载->验证->准备->解析->初始化->使用->卸载几个阶段,具体过程暂不概述)。
  2. 在类加载检查通过后,接下来便为新生对象分配内存。对象所需的内存的大小在类加载完成后就已经确定,因此分配内存阶段只需将一块确定大小的内存从Java堆中划分出来,赋给对象即可。内存的分配方式主要有两种:

    • "指针碰撞"分配方式。
      此分配方式要求Java堆中的内存是绝对规整的,所有已被占用的内存放在一边,空闲的内存放在另一边,中间存放着一个指针作为分解点的指示器,当分配内存时,只需将指针向空闲内存一方移动一段与对象大小相同的距离,并将该段内存分配给对象即可。

    此分配方法需考虑一个问题,对象创建在JVM中是非常频繁的,在并发情况下,即使仅仅修改一个指针所指向的位置也不是线程安全的,可能出现正在给对象A分配内存,指针还未来得及修改,对象B又使用原来的指针分配内存的情况。为解决该问题,一种方法是对分配内存空间的动作进行同步处理--虚拟机实际采用CAS配上失败重试的方式保证更新操作的原子性;另一种方式是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程需分配内存,就在哪个线程的TLBA上分配,只有TLBA用完并分配新的TLBA时,才需同步锁定。可通过-XX:+/-UseTLBA参数来设定是/否使用TLBA。

    • "空闲列表"分配方式。
      若Java堆的内存分配不规整,即已占用内存与空闲内存相互交错,指针碰撞方式将不能进行,需用空闲列表分配方式。该分配方式要求JVM维护一个空闲列表,记录可用的内存块,在分配时在列表中找到一块足够大的内存分配给对象,并更新列表。
  3. 对象内存分配完成后,接下来JVM需堆对象进行必要的设置,如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。
  4. 必要设置完成后,从JVM角度看,一个新对象已经产生,但从Java程序角度看,<init>方法还未执行,所有字段为零,对象的创建才刚刚开始。一般来说,执行new 指令后会接着执行<init>方法,把对象按照程序员的设定进行初始化,这样,一个完整的对象才创建完成。

对象的内存布局

在JVM中,对象的在内存中的存储布局可分为3块区域:对象头、实例数据与对其填充。

  1. 对象头

    对象头包括两部分信息,第一部分用于存储对象自身的运行时数据,被称为”Mark Word“,包括哈希码、GC分代年龄信息、锁状态标记、线程持有的锁、偏向线程ID等。这部反数据长度为32/64位(分别对应于32/64位虚拟机)。第二部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是属于哪个类的实例。
    
    > 若对象位一个Java数组,在对象头中还需有一块用于标记数组长度的数据,因为虚拟机可通过普通对象的元数据确定对象的大小,但从数组的元数据中无法确定其大小,因此必须显式说明。
    
  2. 实例数据

    实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。
    
  3. 对齐填充

    该部分不是必然存在的,也无特殊含义,仅仅起占位符的作用。因为HotSpot VM的自动内存管理机制要求对象的起始地址8字节的整数倍,即对象的大小必须为8字节的整数倍。对象头部分正好为8字节的倍数(1倍或2倍),因此,当对象实例数据部分未对齐时,需通过对齐填充补全。
    

对象的访问定位

对象建立后,就要使用,Java程序通过栈上的reference数据来操作堆上的具体对象。由于reference类型在Java虚拟机规范中只规定了一个指向具体对象的引用,并未定义这个引用通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问访问方式也是取决于虚拟机实现而规定的。目前,主要有两种访问方式。

  1. 使用句柄访问
    若使用句柄访问,则在Java堆中开辟一块内存作为句柄池,reference中存放的是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址。示意图如下:

e0630ed4eb061d22689c57206c88d45f.png

该种访问方式最大的好处是reference中存放的是稳定的句柄地址,在对象移动时只会改变句柄中的实例数据指针,而reference无须改变

  1. 直接指针访问
    此种访问方法reference中存放的直接是对象的地址。示例图如下:

    a87170ed566fc1604c468c7812ec538b.png

该种访问方式最大的好处就是速度快,节省了一次指针定位的时间开销。HotSpot虚拟机采用来该方式进行对象访问。