一、Java 创建对象的完整过程
Java 中通过 new
创建对象,其背后是 JVM 的一系列操作,主要包括:
1.1 类加载检查
当代码中出现 new
指令时,JVM 首先会检查这个类是否已经被加载、解析和初始化过。
- 若未加载,会触发类加载过程(ClassLoader)。
- 加载后才可以为该类分配内存。
1.2 分配内存
为新对象分配内存空间(位于堆内存),有两种方式:
分配方式 | 说明 |
---|---|
指针碰撞(Bump-the-pointer) | 如果内存是规整的(使用 Serial、ParNew 等),通过一个指针一直向后分配即可。 |
空闲列表(Free-list) | 若堆空间不规整(比如 CMS),需要维护一个可用内存块的列表,找到合适位置进行分配。 |
1.3 并发安全保障(重点)
在多线程环境下,为对象分配内存可能出现竞争问题(多个线程争抢分配内存)。
JVM 采用了以下几种机制来保证内存分配的并发安全:
1)CAS + 失败重试
- 指针碰撞方式下,使用 CAS(Compare-And-Swap)操作更新指针;
- 若失败(因为有其他线程成功更新了),则重试,直到成功。
2)Tlab(Thread Local Allocation Buffer)线程本地分配缓冲区
- 每个线程会在堆中分配一块小内存作为私有区域,称为 TLAB;
- 在 TLAB 中分配对象几乎不需要加锁,效率高;
- TLAB 用完再申请,或者直接走共享堆的 CAS 方式。
-XX:+UseTLAB
参数用于开启 TLAB(默认开启)。
1.4 对象初始化
JVM 完成内存分配后,会进行以下操作:
① 将分配的内存空间清零(不包括对象头)
② 设置对象头(包括哈希码、GC 分代年龄、类型指针等)
③ 执行 <init>
构造方法初始化对象内容
二、JVM 对象分配时的内存结构
对象内存结构包含:
- 对象头(Header)
- Mark Word:存储哈希值、GC 信息、锁状态等;
- Class Pointer:指向对象的类型元数据(即 class 对象);
- 实例数据(Instance Data)
- 包含类中定义的字段内容;
- 对齐填充(Padding)
- 为了满足内存对齐要求(通常是8字节倍数)。
三、对象创建的 JVM 字节码体现
使用 javap -v 类名.class
查看,可以发现:
new com.example.User
invokespecial #构造方法
其中:
new
:分配内存并创建引用;dup
:复制栈顶引用;invokespecial
:调用构造方法初始化对象。
四、对象创建方式总结
方式 | 是否走构造函数 | 是否可控 |
---|---|---|
new |
✅ 是 | ✅ |
Class.newInstance() |
✅ 是 | ❌(必须有无参构造) |
Constructor.newInstance() |
✅ 是 | ✅(可选构造) |
clone() |
❌ 否(浅拷贝) | ✅ |
反序列化 | ❌ 否(自动恢复) | ✅ |
五、拓展:逃逸分析与栈上分配
配合 JIT 编译器优化,对未逃逸的对象可优化为栈上分配,从而避免堆分配和 GC。
需开启参数:
-XX:+DoEscapeAnalysis
-XX:+EliminateAllocations
六、面试高频问题总结
Q1:Java 中 new 一个对象经历了哪些步骤?
- 类加载检查;
- 内存分配(TLAB/CAS);
- 对象头初始化;
- 构造函数初始化。
Q2:JVM 如何保证对象分配的并发安全?
- TLAB 本地分配;
- CAS 保证共享分配指针的原子性。
Q3:对象是一定分配在堆上吗?
不是。如果开启逃逸分析、对象未逃逸,有可能在栈上分配,提高效率。
参考
《深入理解Java虚拟机》 第三版 - 周志明
https://book.douban.com/subject/34907497/
OpenJDK 官方文档:对象分配与内存布局
https://openjdk.org/
TLAB 配置详解(Oracle 官方)
https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
Java 内存模型 JSR-133 规范
https://jcp.org/en/jsr/detail?id=133
JVM 源码阅读参考(HotSpot)
https://github.com/openjdk/jdk