1. 类加载检查
- 触发条件:当遇到
new
指令时,JVM首先检查该指令的参数(类符号引用)是否已在常量池中。 - 检查内容:
- 类是否已被加载、解析和初始化。
- 若未加载,则触发类加载过程(加载 → 验证 → 准备 → 解析 → 初始化)。
- 目的:确保类元数据可用,避免后续操作因类未定义而失败。
2. 内存分配
- 分配方式:
- 指针碰撞(Bump the Pointer):适用于内存规整的堆(如Serial、ParNew收集器)。
通过移动指针划分内存,分配速度快(仅需指针移动)。 - 空闲列表(Free List):适用于内存不规整的堆(如CMS收集器)。
维护可用内存块列表,分配时搜索足够大的空间。
- 指针碰撞(Bump the Pointer):适用于内存规整的堆(如Serial、ParNew收集器)。
- 线程安全:
- TLAB(Thread Local Allocation Buffer):为每个线程预分配堆内存区域,避免CAS竞争。
- CAS重试:当TLAB不足时,使用CAS同步分配。
3. 初始化零值
- 操作内容:将对象的内存空间初始化为零值。
- 基本类型字段:
int
→0
,boolean
→false
。 - 引用类型字段:
null
。
- 基本类型字段:
- 目的:确保对象字段在不显式初始化时也能直接使用。
4. 设置对象头(Object Header)
- 对象头结构(64位JVM):
- Mark Word:存储哈希码、GC分代年龄、锁状态(偏向锁/轻量级锁/重量级锁)等信息。
- Klass Pointer:指向类元数据,确定对象类型。
- 示例:
|---------------------------| | Mark Word (64位) | |---------------------------| | Klass Pointer (32位) | |---------------------------|
5. 执行 <init>
方法
- 步骤:
- 初始化父类:递归调用父类构造方法(
super()
)。 - 实例变量赋值:按代码顺序执行显式初始化和构造代码块。
- 构造器代码:执行用户编写的构造方法逻辑。
- 初始化父类:递归调用父类构造方法(
- 目的:完成对象按业务需求的初始化。
内存分配优化策略
策略 | 说明 | 适用场景 |
---|---|---|
TLAB分配 | 线程私有内存区域,减少CAS竞争 | 高频创建小对象的场景 |
逃逸分析 | 若对象未逃逸方法,可能在栈上分配或标量替换 | 方法内部临时对象(JIT优化) |
大对象直接进入老年代 | 避免在新生代频繁复制(通过 -XX:PretenureSizeThreshold 设置阈值) | 大数组、大字符串等 |
对象内存布局
区域 | 内容 | 大小(64位JVM) |
---|---|---|
对象头(Header) | Mark Word(锁状态、哈希码等) + Klass Pointer(类元数据指针) | 12字节(开启压缩指针) |
实例数据(Instance Data) | 对象实际字段值(包括父类继承字段) | 由字段类型和数量决定 |
对齐填充(Padding) | 补齐对象大小为8字节的整数倍 | 0~7字节 |
常见问题与解决方案
-
内存分配失败:
- 触发GC:当Eden区空间不足时,触发Minor GC。
- OOM处理:若GC后仍无法分配,抛出
OutOfMemoryError
。
-
线程竞争:
- TLAB优化:通过
-XX:+UseTLAB
启用(默认开启),减少CAS冲突。
- TLAB优化:通过
-
对象初始化顺序:
- 字段默认值 → 显式赋值 → 构造器代码:确保初始化符合Java规范。
总结
Hotspot虚拟机通过 类加载检查 → 内存分配 → 初始化 → 对象头设置 → 构造方法调用 的流程创建对象,结合 TLAB、逃逸分析 等优化策略,平衡性能与安全性。理解这一过程有助于优化代码(如减少大对象创建)和排查内存问题(如OOM)。