1. 对象的创建

1. 当虚拟机遇到一条 new 指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用。

2. 检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,则需要先执行相应的类加载过程。

3. 虚拟机为新生对象分配内存空间。

其中 java 堆分配内存空间的方式主要有两种:

指针碰撞(假设 java 堆中内存是绝对完整的):所有用过的内存都放在一边,空闲的内存放在一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

空闲列表(已使用的内存和空闲的内存交错):虚拟机就必须维护一个列表,记录上那些内存块是可用的,在分配的时候从列表中找到一块足够的空间划分给对象实例,并更新列表上记录。

选择哪种方式有 java 堆是否规整而定,而 java 堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。

非原子性操作可能出现并发问题,解决方式有两种方案:一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在 java 堆中预先分配一小块内存,称为本地线程分配缓冲(Thread  Local Allocation Buffer,TLAB)。那个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来决定。

4. 内存分配后,虚拟机需要将分配到的内存空间都初始化为零值。

如果是有TLAB,这一步则提前至TLAB分配前进行。保证了对象的实例字段在 java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型对应的零值。

5. 设置对象头信息。

例如:对象是那个类的实例、类的元数据信息、对象的hashCode、对象的GC分代年龄信息。

6.执行对象的 <init> 方法。



2. 对象的内存布局

在Hotspot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充。


2.1 对象头

Hotspot虚拟机的对象头包括两部分信息:自身运行时数据和类型指针。

自身的运行数据包含 HashCode、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳。这些数据的长度在32位和64位的虚拟机中分为 32bit 和 64bit。官方称为 Mark Word。

类型指针,即对象志向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。


2.2 实例数据

实例数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型字段内容。无论是从父类集成下来的,还是在子类中定义的,都需要记录下来。


2.3 对齐填充

对齐填充不是必然存在的,也没有特别的含义,仅仅起着占位符的作用。

由于Hotspot VM 的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。



3. 对象的访问定位

当我们在堆创建对象实例后,就要通过虚拟机栈中的 reference 类型数据来操作堆上的对象,现在主流的访问方式有两种(Hotspot虚拟机使用第二种)

1. 使用句柄访问,那么java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。

5.JVM基础-对象的创建、内存布局和访问定位_对象布局

2. 使用直接指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而 reference 中存储的直接就是对象地址。 

5.JVM基础-对象的创建、内存布局和访问定位_对象布局_02