JVM-堆中的对象


前言

​ 使用 Java 语言编写程序时,我们可以通过 new 关键字、反射、复制、反序列化等方式创建对象。关于 JVM 是如何产生这个对象的,对象是以什么样的形式存储在堆中的,以及对象是如何访问的,下面将以 HotSpot 虚拟机、new 关键字创建方式为例,进行探讨。

创建

类加载

使用 new 关键字创建对象,会产生一条 new 指令。虚拟机遇到 new 指令,会先去检查这个 new 指令的参数,在常量池中能否找到一个类的符号引用。然后去验证这个符号引用表示的类是否已被加载、链接、初始化。如果没有,需要先去进行类加载

内存分配

一个对象所需的内存大小,在类加载完成后便可以确定。为对象分配内存,就是在堆中指定一块内存供对象使用。常用的分配方式有指针碰撞、空闲列表。

  • 指针碰撞(Bump The Pointer)

    在堆内存规整的情况下,空闲的内存被放在一边,另一边则是正在使用的内存,中间则用一个指针作为分界点。分配内存时,仅需要将指针,向空闲一边移动对象大小的距离。

  • 空闲列表(Free List)

    在堆内存不规整的情况下,虚拟机需要维护一个列表,用于记录哪些内存是空闲的。分配内存时,在记录表找出一块足够大的内存给对象,并且更新记录表。

堆内存是否规整,由垃圾收集器是否有空间压缩整理(Compact)的能力决定。像 SerialParNew 等带有压缩整理的收集器,采用指针碰撞实现内存分配,既简单又高效。而 CMS 这类基于清理 (Sweep)算法的收集器,理论上(采用 Linear Allocation Buffer 技术,可以做到指针碰撞分配内存)只能采用空闲列表这种较复杂的方式来分配内存。

内存分配的线程安全

在虚拟机中,对象创建频繁,就算是修改一个指针所指向的位置,在并发的情况下也是线程不安全的。解决线程安全这个问题的方案有两种。

  • 对分配内存空间的动作进行同步处理 —— 采用 CAS 加上失败重试的方式来保证更新操作的原子性。
  • 对分配内存空间的动作按线程分离到不同空间进行 —— 每个线程在堆中预先划分出一小块内存,称作本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),内存分配操作在这个线程隔离的小内存进行,当缓冲区用完,需要重新分配时才进行同步锁定。虚拟机通过参数 -XX:+/-UseTLAB 来决定是否使用 TLAB

内存初始化

需要对分配到的内存空间初始化为零值(除 Object Header),如果启用了 TLAB,则可将初始化提前到 TLAB 分配时进行。

对象头设置

对象头包含类的元数据信息、对象哈希码(延迟到调用 Object#hashCode() 计算)、对象的 GC 分代年龄、锁状态标志等。虚拟机会做对象头必要信息的设置。

构造对象

目前,虚拟机已经创建了一个可用的对象。但此时的对象,并没有符合 Java 程序的意图 —— 初始化。

虚拟机遇到 new 关键字会生成两条字节码指令,newinvokespecialnew 指令就是上述的操作,invokespecial 则是调用 <init>() 方法的。只有在执行 <init>() 方法后,这个对象才算构造完成。

内存布局

HotSpot 虚拟机中,对象划分为三个部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

对象头(Header)

对象头包含两类信息。

第一类存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。这部分信息官方称为 “Mark Word”

另一类信息是类型指针,可确定该对象是哪个类的实例。

实例数据(Instance Data)

程序所定义的字段就保存在实例数据中,存储顺序受虚拟机分配策略参数 -XX: FieldsAllocationStyle 和字段在代码中定义顺序的影响。

HotSpot 虚拟机默认的分配顺序为 longs/doublesintsshorts/charsbytes/booleansoops(Ordinary Object Pointers,OOPs),满足该前提的情况下,父类变量在子类前。

HotSpot 虚拟机参数 +XX: CompactFieldstrue (默认 true),子类中较窄的变量也允许插入到父类变量的空隙中,以节省空间。

对齐填充(Padding)

无特别含义,起着占位符的作用。由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍。也就是说,任何对象的大小都必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),所以,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

访问定位

Java 程序能够通过栈的 reference 来操作堆上的对象。《Java虚拟机规范》 仅规定 reference 类型为一个指向对象的引用,并没有定义这个引用该以什么方式去定位、访问堆中对象的具体位置。目前主流的访问方式主要有使用句柄和直接指针两种:

使用句柄

reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。

使用句柄访问对象

直接指针

速度更快,它节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。HotSpot 主要使用直接指针进行对象访问。

使用直接引用访问对象

参考

​ [1] 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版). 2.3 HotSpot虚拟机对象探秘