Java 是一门面向对象的编程语言,Java 程序在运行过程中时时刻刻都有对象被创建出来。
———— 周志明《深入理解Java虚拟机》

类加载检查

我们所说的对象的创建,通常是 new 关键字。当 JVM 遇到一条 new 指令后,会先在常量池中检查,看能否定位到一个类的符号引用。

若能找到,则检查这个类是否已被加载、解析和初始化过。若这些过程未被执行,则需要先执行这些操作。

分配内存

这里主要关注的是在(Heap)上的对象内存分配。当对象对应的类已经通过加载检查后,其占用的内存大小是完全确定的,所以内存分配的工作相当于在堆上划出一块确定大小的空间。

分配方式

根据堆的布局情况,又有两种不同的分配方式。

  1. 绝对规整

    堆是绝对规整的,意味着其中用过的内存放在一边,其他未使用的内存放在另外一边,分界线由一个指针作为指示器。这种情况下,分配内存的行为仅仅就是把指针向空闲内存一边移动相应距离,这种分配方式也被形象地成为“指针碰撞”(Bump the Pointer)。

  2. 非绝对规整

    非绝对规整的话,使用过的内存和空闲内存交错排布,无法使用指针碰撞。JVM 此时需要维护一个列表,列表上记录着哪些内存块是被占用的,哪些是空闲的。分配时,需要在列表上找到一块足够大的内存空间分配给对象,同时更新列表。这种分配方式又被称为“空闲列表”(Free List)。

决定因素

Java 堆的规整与否与采用的 GC 是否带有压缩整理功能有关。

在使用 Serial, ParNew 等带 Compact 过程的收集器时,Java 堆是规整的,其内存分配方式是指针碰撞;而在使用 CMS 这类基于 Mark-Sweep 算法的收集器时,一般是采用空闲列表的方式。

线程安全

由于创建对象在 JVM 中是很频繁的行为,修改一个指针的位置在并发情况下有时候并不是线程安全的。

解决线程安全问题的方案有两种:

  1. 同步处理

    对分配内存空间的动作进行同步。实际上,JVM 采用 CAS(Compare And Swap,比较版本号后,若版本号不是最新,则认定本次 CAS 操作失败,反之允许提交)配合失败重试的方式来保证更新操作的原子性。

  2. TLAB

    TLAB(Thread Local Allocation Buffer,本地线程分配缓冲) 是 Java 堆为每个线程预先分配的一小块内存。每个线程在各自的 TLAB 上分配,当 TLAB 用完需要新的内存时。才需要同步操作。

初始化

内存分配完成后,JVM 将分配到的内存空间都初始化为零值。若是使用了 TLAB,初始化零值的操作也可以提前到分配 TLAB 时进行。初始化操作保证了对象的实例字段可以在 Java 代码中不赋初值而直接使用,但此时访问到的是对应数据类型的零值。

同时,JVM 还要对对象进行一些必要的设置,如确定该对象所属的类、找到该类的元数据的方法(元数据,meta-data,即定义数据的数据,比如一个 Person 模型,其中有 name、age 等字段,这些字段即时元数据)、哈希码、GC 分代等。这些信息都是储存在对象的对象头(Object Header)中。

–本文参考 周志明《深入理解Java虚拟机:JVM高级特性与最佳实践(第二版)》–