JVM - 浅谈对象的内存管理与实例化

一、对象的内存布局

以HotSpot虚拟机为例,对象在内存中的结构可以分为 3 部分:

  • 对象头(header)
  • 实例数据(instance data)
  • 对齐填充(padding)

1. 对象头

对象头的结构大体相似,但不同的JVM的具体实现之间略有差别。一般来说,对象头都包含了标记字类型指针两部分信息,如果对象是数组,还会额外包含数组长度信息。

(1)标记字

存储对象自身运行时的数据(即状态),包括哈希码、GC 分代年龄、锁状态标志、线程持有锁、偏向线程 ID、偏向时间戳等。它们的存储结构类似 C 语言中的位字段,官方称为 Mark Word标记字)。标记字以作为基本存储单元,即在32位虚拟机中,数据长度位 32bit;而在64位虚拟机中,数据长度位64bit。

以 32bit 虚拟机为例,有固定的 2bit 用于储存锁标志位,随着锁标志位的值不同,其它为存储的内容与位长度也不同。这点类似于 C 语言中的联合结构(union),且联合的每一个成员都是位字段结构。

(2)类型指针

类型指针即对象指向它的类元数据(class metadata)的指针,虚拟机通过该指针确定这个对象是哪个类的实例。但需要注意的是:并非所有虚拟机实现中都会在对象头包含类型指针,可以采用其它方式保留对象的信息

(3)数组长度

Java中,数组也属于对象,那么理所当然地需要维护数组的长度。

2. 实例数据

实例数据即对象的字段(或称为成员变量)存储的数据信息,包含了从父类继承及自己定义的所有字段。且字段在内存中存储的顺序并不等于类中的定义顺序,它受到虚拟机策略的影响(主要考虑到内存对齐以及使用率的问题)。

3. 对齐填充

类似于 C 语言中的结构体 struct 的内存对齐,Java 对象的内存位置也需要对齐。

常用的HotSpot虚拟机要求每个对象的起始地址位 8 字节的整数倍,也就是说,若一个对象的结束地址不是 8 字节的整数倍,则需要占位符填充以保证对齐。

二、对象的访问定位

JVM规定,需要通过栈上的引用reference)来操作具体对象。对于该规定,目前有两种主流的实现方式。

  • 通过句柄handler)实现
  • 通过直接指针direct point)实现

1. 通过句柄(handler)实现

该种方式会在堆中划出一块句柄池内存空间,每个栈上的引用直接指向句柄池中的句柄,而句柄中又会维护对象指针类型指针。使用句柄的优点是:栈上的reference存储稳定的句柄地址,GC造成的对象移动只会导致句柄中相应的只想地址改变,而reference本身所在的地址不变。如下图所示:

JVM - 浅谈对象的内存管理与实例化

2. 通过直接指针(direct point)实现:

即在对象的对象头中维护类型指针。栈的reference指向对象,而向对象头中的类型指针指向对象类型数据。使用直接指针的优点是:对象访问速度快,节省了指针二次寻址的开销,也节省了句柄带来额外的内存开销。如下图所示:

JVM - 浅谈对象的内存管理与实例化

三、对象的创建过程

1. 类加载

  • 检查到 new 指令;
  • JVM检查在常量池中是否有该类的符号引用,包括该符号引用代表的类是否已被加载解析初始化
  • 若没有,则先加载类;否则,直接创建对象。

显然,只有对象首次被实例化的时候才会执行加载类过程。

2. 内存分配

(1) 内存分配的方式

一个对象所需内存在类加载时便可确定,内存分配方式有两种:

  • 指针碰撞法:若Java堆中内存是绝对规整的,所有用过内存都放到一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那分配内存就是把指针向空闲空间那边挪动一段与对象大小相等的距离;
  • 空闲列表法:若堆中内存不规整,无法通过简单的移动指针分配内存。这种情况下虚拟机会维护一个列表记录那些内存可用,分配时查找并更新列表。

使用哪种方式取决于内存是否规整。而内存是否规整取决于垃圾收集器GC 算法。
典型如 serial、parnew 这两种垃圾收集器,它们在执行GC时带有压缩整理功能,因子系统会采用指针碰撞的方式分配内存;而对于 CMS 这种基于 Mark-Sweep(标记-清除)算法的垃圾收集器,则会采用空闲列表法。

(2) 内存分配的安全

注意:若多个线程同时申请分配内存,如果不加以同步控制,则会导致内存分配错误。不同的虚拟机会采用不同的机制规避线程安全问题:

  • 同步锁定:通过CAS配上失败充实的方式保证更新操作的原子性。注:CAS,即CPU硬件同步原语,全称为 compare and swap(比较并交换),若比较不对则失败;
  • TLAB:即线程分配缓冲区。在堆中预先为每个线程分配一小块内存,线程在各自分配的内存上进行内存分配来保证安全。只有当TLAB用尽并申请新的TLAB时,才进行同步锁定。

3. 内存初始化

内存初始化指的是:将对象分配到的内存的所有位置重置为0(不包含对象头)。若对象是通过TLAB分配的,该过程会提前至内存分配步骤中执行。

4. 对象头初始化

设置对象的对象头信息。

5. 对象实例数据初始化

设置对象的实例数据信息,即成员变量值。只有这步完成了,一个真正的对象才完成了实例化