对象创建
书接上回,我们接着来讲解对象创建。
JVM 的内存分配的重中之重就是 Heap 上对象的分配。它是最大,同时也最常访问的一块内存。
以下内容即为 Hotspot VM 在创建对象时所做的工作:
在新建对象时,JVM 会首先检查常量池中是否有对应类的符号引用,若无,则执行响应的类加载过程。
之后 JVM 将会为新生对象分配内存,而对象所需内存在类加载完毕后是完全确定的。所以实质上就是从 Heap 取一块固定大小的内存。
- 取 Heap 有多种不同实现,若假设内存绝对规整,即使用过的内存和未使用内存界限分明,中间使用一个指针作为分界点指示器。若要分配内存,则只需要挪动对应的指针距离,这种方式叫「指针碰撞」(Bump The Pointer)。
- 然而,维护规整的内存价格是高昂的。因此,很多时候使用和未使用内存是交错的,也就无法指针碰撞。那么 JVM 就必须维护一个列表,从列表中查找可用的内存并分配。这种方式名为「空闲列表」(Free List)。
- 选择哪种方式是由 JVM 实现决定的。有些 GC ,如 Serial、ParNew,使用 Compact 方式垃圾收集,因此对应的分配算法即为指针碰撞;而诸如 CMS 这种,使用 Sweep 算法,则需要使用 free list。
- 另外,对象创建需要考虑线程安全。有的虚拟机使用
CAS 原子操作 + 失败重试机制;而有的虚拟机还使用了
TLAB(Thread Local Allocation
Buffer,本地线程分配缓存),先在缓存中分配,用尽时再同步锁定。可以通过
-XX:+/-UseTLAB
参数尝试设定。
内存分配完毕后,虚拟机会将对象初始化为零值。以避免访问到意外的数值。在 native 语言中常常需要开发者手动初始化。
之后,JVM 会设置基本的对象信息,例如对象的类型信息、元数据、哈希码、GC 分代等存放在对象头(Object Header)中的数据。还涉及对锁的优化。
Done,虚拟机需要做的工作已经完成了。之后会执行对应的
<init>()
方法。
对象布局
Hotspot VM 中,对象的 Heap 存储布局可以分为三部分:对象头、实例数据、对齐填充(Header、Instance Data、Padding)。
对象头可以再细分两类:
- Mark Word 储存对象自身的运行时数据,如 HashCode、GC 分代年龄、锁状态、持有的锁等,若未开启压缩指针,大小则对应操作系统位数 32 或 64 位。Mark Word 被设计为动态的数据结构,以尽量存储更多的数据。
- 另一部分就是类型指针了。就是指向对象类型元数据的指针,JVM 通过此指针确定该对象是哪个类的实例。当然也不是所有虚拟机查找元数据都需要经过对象本身。另外对于数据,还需要存储数组长度。
接下来的实例数据部分,是对象的有效定义信息,即各种在代码中定义的字段内容,包括父类和自身的定义。
在 C 中,结构体字段存放的顺序,会影响运行时的对齐行为。而 JVM
中是由具体虚拟机实现和定义顺序共同决定的。可以通过
-XX:FieldsAllocationStyle
参数指定策略。
Hotspot VM 默认的分配顺序为
long、doube、int、short、char、oops(Ordinary Object Pointers,
OOPs)。可以看出策略是让等宽的数据靠在一起。此外,+XX:CompactFields
为 true 时(默认为
true)会允许父类和子类混合顺序,否则就会按照父类在前子类在后的顺序分配。
为了使得 CPU 寻址时获得最高效率。HotSpot VM 要求对象起始地址为 8 byte 的倍数。因此非此倍数的对象会添加 padding。
对象访问
对象创建就是为了访问的。JVM 规范定义,Stack 上具有 Reference 数据来操作堆上的实体对象。当然了,这个 Reference 如何实现,也是看虚拟机怎么做。常用的有两种实现:
- 使用句柄(Handle)访问。Heap 中会划出一段句柄池,ref 的就是对象的句柄地址,其中存有对象实例和对象类型的数据地址。
- 直接通过指针(Pointer)访问。此时 Heap 的内存布局则需要考虑如何放置类型数据地址。但也减少了一次间接访问的开销。
两种方式各有优势,使用句柄可以稳定局部变量。在对象移动时(GC 时常常移动对象)会只需要改变句柄中的指针,而无需修改 ref 本身。
而直接指针最大的好处就是速度快。节省了一次定位开销。更何况对象访问是十分频繁的行为。HotSpot VM 就是用的直接指针访问方式。
本节内容就到这里,下一篇文章将会讲解 GC 相关的内容。