前言
使用 Java 语言编写程序时,我们可以通过 new 关键字、反射、复制、反序列化等方式创建对象。关于 JVM 是如何产生这个对象的,对象是以什么样的形式存储在堆中的,以及对象是如何访问的,下面将以 HotSpot 虚拟机、new 关键字创建方式为例,进行探讨。
创建
类加载
使用 new 关键字创建对象,会产生一条 new 指令。虚拟机遇到 new 指令,会先去检查这个 new 指令的参数,在常量池中能否找到一个类的符号引用。然后去验证这个符号引用表示的类是否已被加载、链接、初始化。如果没有,需要先去进行类加载。
内存分配
一个对象所需的内存大小,在类加载完成后便可以确定。为对象分配内存,就是在堆中指定一块内存供对象使用。常用的分配方式有指针碰撞、空闲列表。
-
指针碰撞(Bump The Pointer)
在堆内存规整的情况下,空闲的内存被放在一边,另一边则是正在使用的内存,中间则用一个指针作为分界点。分配内存时,仅需要将指针,向空闲一边移动对象大小的距离。
-
空闲列表(Free List)
在堆内存不规整的情况下,虚拟机需要维护一个列表,用于记录哪些内存是空闲的。分配内存时,在记录表找出一块足够大的内存给对象,并且更新记录表。
堆内存是否规整,由垃圾收集器是否有空间压缩整理(Compact)的能力决定。像 Serial、ParNew 等带有压缩整理的收集器,采用指针碰撞实现内存分配,既简单又高效。而 CMS 这类基于清理 (Sweep)算法的收集器,理论上(采用 Linear Allocation Buffer 技术,可以做到指针碰撞分配内存)只能采用空闲列表这种较复杂的方式来分配内存。
内存分配的线程安全
在虚拟机中,对象创建频繁,就算是修改一个指针所指向的位置,在并发的情况下也是线程不安全的。解决线程安全这个问题的方案有两种。
- 对分配内存空间的动作进行同步处理 —— 采用 CAS 加上失败重试的方式来保证更新操作的原子性。
- 对分配内存空间的动作按线程分离到不同空间进行 —— 每个线程在堆中预先划分出一小块内存,称作本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),内存分配操作在这个线程隔离的小内存进行,当缓冲区用完,需要重新分配时才进行同步锁定。虚拟机通过参数 -XX:+/-UseTLAB 来决定是否使用 TLAB。
内存初始化
需要对分配到的内存空间初始化为零值(除 Object Header),如果启用了 TLAB,则可将初始化提前到 TLAB 分配时进行。
对象头设置
对象头包含类的元数据信息、对象哈希码(延迟到调用 Object#hashCode() 计算)、对象的 GC 分代年龄、锁状态标志等。虚拟机会做对象头必要信息的设置。
构造对象
目前,虚拟机已经创建了一个可用的对象。但此时的对象,并没有符合 Java 程序的意图 —— 初始化。
虚拟机遇到 new 关键字会生成两条字节码指令,new、invokespecial。new 指令就是上述的操作,invokespecial 则是调用 <init>() 方法的。只有在执行 <init>() 方法后,这个对象才算构造完成。
内存布局
HotSpot 虚拟机中,对象划分为三个部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头(Header)
对象头包含两类信息。
第一类存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。这部分信息官方称为 “Mark Word”。
另一类信息是类型指针,可确定该对象是哪个类的实例。
实例数据(Instance Data)
程序所定义的字段就保存在实例数据中,存储顺序受虚拟机分配策略参数 -XX: FieldsAllocationStyle 和字段在代码中定义顺序的影响。
HotSpot 虚拟机默认的分配顺序为 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),满足该前提的情况下,父类变量在子类前。
HotSpot 虚拟机参数 +XX: CompactFields 为 true (默认 true),子类中较窄的变量也允许插入到父类变量的空隙中,以节省空间。
对齐填充(Padding)
无特别含义,起着占位符的作用。由于 HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍。也就是说,任何对象的大小都必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),所以,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
访问定位
Java 程序能够通过栈的 reference 来操作堆上的对象。《Java虚拟机规范》 仅规定 reference 类型为一个指向对象的引用,并没有定义这个引用该以什么方式去定位、访问堆中对象的具体位置。目前主流的访问方式主要有使用句柄和直接指针两种:
使用句柄
reference 中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。
直接指针
速度更快,它节省了一次指针定位的时间开销,由于对象访问在 Java 中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本。HotSpot 主要使用直接指针进行对象访问。
参考
[1] 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版). 2.3 HotSpot虚拟机对象探秘