在 Java 中对象的创建方式有多种,最常见的就是通过 new 关键字,但是无论用什么方式,JVM 底层都是一样的。
1. 类加载检查
当代码中使用 new 关键字创建对象时,JVM 首先会检查所要创建对象的类是否已经被加载。如果该类尚未被加载,JVM 会启动类加载机制,按照类加载器的委托模型查找并加载相应的类。类加载过程分为加载、验证、准备、解析和初始化五个阶段。
2. 分配内存空间
一旦确认类已加载,JVM 就会为新对象分配内存空间。对象所需内存的大小在类加载完成后就已经确定。内存分配方式主要有两种:
- 指针碰撞:如果 Java 堆中的内存是规整的,即已使用的内存和未使用的内存分别在两端,中间有一个指针作为分界点。那么分配内存时,只需将指针向未使用的内存方向移动与对象大小相等的距离即可。比如带有内存整理过程的垃圾回收器(如 Serial、ParNew 等)在垃圾回收时会进行内存紧凑化操作,将堆内存整理成规整的状态。当堆内存经过这些垃圾回收器的内存整理操作后,就满足了指针碰撞的内存分配要求。此时,在分配新对象内存时,只需将分界指针向未使用的内存方向移动与对象大小相等的距离即可完成内存分配,无需进行额外的空闲内存块查找和管理,大大提高了内存分配的效率。
注意:指针碰撞是一种在内存规整情况下进行对象内存分配的高效方式,但在多线程环境下使用指针碰撞会引发并发问题。常见的解决方案有:
- CAS(compare and swap)
- 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
- 空闲列表:如果 Java 堆中的内存不是规整的,已使用的内存和未使用的内存相互交错,那么 JVM 就需要维护一个记录哪些内存块是空闲的列表(free list)。分配内存时,从列表中找到一块足够大的空间划分给对象实例,并更新列表。这种方式适用于使用 CMS 这种基于标记清除 (Mark - Sweep) 算法的垃圾回收器,因为这种算法回收后会产生内存碎片。
3. 初始化零值
内存分配完成后,JVM 会将分配到的内存空间初始化为零值(不包括对象头)。这一步操作保证了对象的实例变量在 Java 代码使用之前就已经有了一个确定的初始值,比如 int 类型初始化为 0,boolean 类型初始化为 false,引用类型初始化为 null 等。这样做的好处是,当对象的构造函数中没有显式地对某些变量进行初始化时,也能保证其有一个默认的初始值,避免程序出现错误。
4. 设置对象头
在HotSpot虚拟机中,对象的内存布局可以分为 3 部分 :对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。在初始化零值之后,JVM 会设置对象头信息。对象头包含两部分数据:
- 运行时元数据:例如对象的哈希码(HashCode)、对象的分代年龄、锁状态标志等信息。这些信息用于 JVM 在运行时对对象进行管理和操作,比如在垃圾回收过程中判断对象是否存活,在多线程环境下处理对象的锁机制等。
- 类型指针:指向对象所属类的元数据的指针,通过这个指针,JVM 可以知道该对象是哪个类的实例,从而能够调用该类的方法等。如果对象是一个数组,对象头中还会包含数组的长度。
对象结构
32 位 JVM 的 Mark Word
64 位 JVM 的 Mark Word
5. 执行方法
最后一步是执行对象的构造函数(init 方法)。JVM 会通过
最后,来个图总结下