目录
类加载和对象创建过程
在 Java 世界中,“类加载”和“对象创建” 是两个看似基础却异常关键的概念。你可能已经写过无数个 new,但你是否真正了解在这背后 JVM 为你做了哪些“魔法操作”?
无论是面试官常问的“类是如何加载的?对象是怎么创建的?”,还是开发中遇到的各种奇妙现象,如类初始化顺序异常、对象未赋值问题,其实都离不开对这两个核心流程的深入理解。
本篇文章将带你从 JVM 的视角出发,全面剖析 Java 类加载过程与对象创建机制,让你对这两个过程形成系统认知,并掌握关键细节与底层原理。不仅能帮你面试答得漂亮,更能在实际开发中写出更稳定、更高效的代码。
类加载过程
系统加载 Class 类型的文件主要三步:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。
1. 加载
- 通过全类名获取定义此类的二进制字节流。
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表该类的
Class
对象,作为方法区这些数据的访问入口。
执行结果:生成 java.lang.Class
对象。
2. 验证
- 确保字节码符合 JVM 的安全规范,防止恶意代码运行。
- 主要验证:文件格式(CLass 文件格式检查)、元数据(字节码语义检查)、字节码结构(程序语义检查)、符号引用(类的正确性检查)等。
保证 .class
文件的合法性和安全性。
3. 准备
- 为类变量(
static
修饰的字段)分配内存并设置默认值(非初始化值)。
例如:
- 准备阶段只给类变量分配内存,不会给实例变量分配内存
- 准备阶段正常只会赋零值。准备阶段后,a = 0
static int a = 10; // 此阶段 a=0
如果是加了final这种,会直接赋初始值,准备阶段后 a = 10
static final int a = 10;
4. 解析
- 将常量池中的符号引用替换为直接引用。
- 符号引用:字符串形式,如“com/Example”
- 直接引用:内存地址指向实际类或方法
5. 初始化
-
执行类构造器初始化
<clinit>()
方法:<clinit>()
不是你写的代码,而是 JVM 自动生成的特殊方法,它的作用是:把类中所有静态变量的赋值语句和静态代码块按顺序合并在一起,生成一个
<clinit>()
方法。只在类 初始化阶段 执行一次。
- 合并所有静态变量的赋值动作和静态代码块
-
按顺序执行:静态变量赋值 → 静态代码块
static int a = 10;
static {
a = 20;
}
最终 a == 20
只有在初始化阶段才会真正执行程序编写的代码!
对象创建过程
Java 中对象的创建过程其实远比一个 new
看起来要复杂得多,它背后涉及 JVM 的类加载机制、内存分配、构造方法调用等多个步骤。
一、Java 创建对象的方式
常见的创建方式:
MyClass obj = new MyClass(); // 常见方式
Class clazz = Class.forName("MyClass"); // 反射方式
MyClass obj2 = clazz.newInstance(); // 反射创建
MyClass obj3 = obj.clone(); // clone 创建
MyClass obj4 = (MyClass) input.readObject(); // 反序列化
二、Java 中对象的创建过程(以 new
为例)
MyClass obj = new MyClass();
这个语句背后 JVM 做了 五个主要步骤:
1. 类加载检查
- 首先,JVM 检查
MyClass
类是否已经被加载、连接和初始化(即是否执行过类加载过程中的 7 个阶段)。 - 如果没有,会触发类加载流程,包括执行
<clinit>()
方法。
2. 在堆内存中分配内存
- JVM 会在 Java 堆中为对象分配内存空间,大小由类中的实例变量数量和类型决定。
- 分配方式可能是“指针碰撞”或“空闲列表”策略。**选择那种方式取决于Java堆是否完整,而Java堆是否完整取决于所采用的垃圾收集器是否带有压缩整理功能
内存分配的两种方式
- 指针碰撞:
- 适用场合:堆内存规整(即没有内存碎片)的情况下。
- 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可。
- 使用该分配方式的 GC 收集器:Serial, ParNew
- 空闲列表:
- 适用场合:堆内存不规整的情况下。
- 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。
- 使用该分配方式的 GC 收集器:CMS
选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的
3. 初始化零值(默认值)
- 分配到的内存空间会被初始化为默认值(如 int 为 0,引用为 null,boolean 为 false)。
- 这一步只是分配内存,还没有赋你写的初始值。
4. 设置对象头(元数据)
对象头会记录:
- 对象的所属类元数据(Class 对象指针)
- 对象的哈希码、GC 信息、锁状态
- 用于支持同步、GC 和锁机制
5. 执行构造函数 <init>()
- 执行你写的构造方法(构造器),这个方法叫做
<init>()
,初始化对象字段的实际值。
public class MyClass {
int x = 10;
MyClass() {
x = 20;
}
}
执行顺序:
- 初始化为默认值(x = 0)
- 执行成员变量赋值(x = 10)
- 执行构造器赋值(x = 20)
参考
https://javaguide.cn/java/jvm/memory-area.html#%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%88%9B%E5%BB%BA