知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!
JVM 对象创建与类的生命周期深度解析
一、类的生命周期
在 JVM 中,类的生命周期是对象创建的基础。类的生命周期包括以下阶段:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
每个阶段直接影响对象的创建和内存分配。以下是类生命周期与对象创建的关联分析:
二、对象创建的核心流程
1. 触发类加载(当类未加载时)
- 场景:当首次通过
new指令创建对象时,若类未加载,触发类加载过程。 - 步骤:
- 加载:
- 通过类加载器(ClassLoader)从字节码文件(
.class)加载类信息到方法区。 - 生成
Class对象(存储在堆中),作为方法区数据的访问入口。 - 注意:数组类本身不需要类加载器创建
- 通过类加载器(ClassLoader)从字节码文件(
- 验证:
- 确保字节码符合 JVM 规范(如魔数校验、语法检查)。
- 目的:为了确保class文件的字节流包含的信息符合当前虚拟机的要求
- 准备:
- 为静态变量分配内存并设置默认初始值(如
int初始化为0),不包括实例变量 - 目的:正式为类变量 分配内存并设置类变量初始值的阶段
- 例如:public static int value=123;变量value准备阶段的值是0,而不是123,将123赋值是等到初始化阶段执行的
- 为静态变量分配内存并设置默认初始值(如
- 解析:
- 将符号引用(如类名、方法名)转换为直接引用(内存地址)。
- 加载:
// 示例:首次创建对象时触发类加载
public class MyClass {
public static void main(String[] args) {
// 第一次 new MyObject 时触发 MyObject 类的加载、验证、准备、解析
MyObject obj = new MyObject();
}
}
class MyObject {}
2. 类初始化(执行 <clinit> 方法)
- 触发条件:
- 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化, 则需要先触发其初始化
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父 类的初始化,类和接口不一样,初始化类的时候,不会初始化它 的接口,初始化接口的时候不会初始化它的父接口
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个 类),虚拟机会先初始化这个主类。
- 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后
的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄 所对应的类没有进行过初始化,则需要先触发其初始化
- 操作内容:
- 执行静态变量的显式赋值(如
static int a = 5;)。 - 执行静态代码块(
static { ... })。
- 执行静态变量的显式赋值(如
class MyClass {
static int value = initValue(); // 静态变量赋值
static { System.out.println("静态代码块执行"); } // 静态代码块
static int initValue() {
return 42;
}
}
3. 对象内存分配
- 分配方式:
- 指针碰撞:对象所需内存的大小在类加载完成后可确定,为对象分配空间的任务等同于把一块确定大小的内存从java堆中取出来,假设java堆中内存是绝对完整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为【指针碰撞】。
- 空闲列表:如果java堆中内存不是完整的,已使用的内存和空闲的空间相互交错,那就没有简单的指针碰撞,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候就从列表中找到一个足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为【空闲列表】
- 选择 :选择哪种方式由java堆是否完整决定,而java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。除如何划分空间之外,还由另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下并不是线程安全的,可能出现正在给对象A修改内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况,解决这个问题有两个解决的方案,一种是对内存空间的动作进行同步处理--实际上虚拟机上采用的是CAS配上失败重试的方式来保证更新操作的原子性,另一种值把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定,虚拟机是否要使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
- 并发处理:
- CAS + 失败重试:保证多线程竞争下的原子性。
- TLAB(Thread-Local Allocation Buffer):为每个线程预分配内存区域,避免竞争。
# 指针碰撞分配示例
堆内存布局: |已用|空闲|
指针 → ↑
分配后: |已用|新对象|空闲|
指针 → ↑
4. 对象初始化
- 内存空间零值初始化:
- 所有实例变量初始化为默认值(如
int为0,引用为null)。
- 所有实例变量初始化为默认值(如
- 设置对象头:
- Mark Word:存储哈希码、锁状态、GC 分代年龄等。
- 类型指针:指向方法区的类元数据(压缩指针优化后为 4 字节)。
- 执行
<init>方法:- 调用构造函数,初始化实例变量和代码块。
class MyObject {
private int value; // 默认初始化为0
{ value = 42; } // 实例代码块初始化
public MyObject() {
System.out.println("构造函数执行");
}
}
三、类的生命周期与对象创建的关联
1. 类的生命周期阶段在对象创建中的体现
| 生命周期阶段 | 对象创建中的操作 | 异常示例 |
|---|---|---|
| 加载 | 加载类字节码到方法区,生成 Class 对象 | ClassNotFoundException |
| 验证 | 确保类字节码安全合法 | VerifyError |
| 准备 | 为静态变量分配内存并赋默认值 | - |
| 解析 | 将符号引用转为直接引用(如方法调用目标地址) | NoSuchMethodError |
| 初始化 | 执行静态代码块和静态变量赋值 | ExceptionInInitializerError |
| 使用 | 创建对象、调用方法、访问字段 | NullPointerException |
| 卸载 | 类不再被引用时,由 GC 回收方法区数据 | - |
2. 类卸载的条件
- 触发条件:
- 类的所有实例已被回收。
- 加载该类的
ClassLoader已被回收。 - 对应的
Class对象未被任何地方引用(如反射)。
- 典型场景:
- 使用自定义类加载器(如 OSGi、热部署框架)加载的类。
// 示例:自定义类加载器加载的类可能被卸载
ClassLoader loader = new MyClassLoader();
Class<?> clazz = loader.loadClass("MyDynamicClass");
// 当 loader 和 clazz 不再被引用时,可能被卸载
四、对象创建中的异常处理
1. 类加载失败
- 原因:类文件缺失、字节码不合法、静态初始化失败等。
- 异常类型:
ClassNotFoundException:类文件未找到。NoClassDefFoundError:类加载后初始化失败。
2. 内存分配失败
- 原因:堆内存不足(OOM)。
- 异常类型:
OutOfMemoryError: Java heap space。
3. 初始化失败
- 原因:构造函数抛出异常、实例代码块错误等。
- 异常类型:
ExceptionInInitializerError。
五、诊断工具与参数
1. 监控类加载与卸载
- JVM 参数:
-XX:+TraceClassLoading:打印类加载日志。-XX:+TraceClassUnloading:打印类卸载日志。
- 工具:
- JVisualVM:查看已加载的类。
- Java Mission Control (JMC):分析类加载事件。
2. 内存分配监控
- JVM 参数:
-XX:+PrintTLAB:打印 TLAB 分配情况。-XX:+PrintGCDetails:监控 GC 对堆内存的影响。
六、总结
-
类的生命周期是对象创建的基础:
- 对象创建前必须完成类的加载、验证、准备、解析和初始化。
- 类的卸载需要严格的条件,通常由自定义类加载器触发。
-
对象创建的核心步骤:
- 类加载检查 → 内存分配 → 内存初始化 → 设置对象头 → 执行构造函数。
-
性能优化方向:
- 减少类加载开销:避免重复加载类(如单例模式)。
- 优化内存分配:使用 TLAB 减少线程竞争。
- 避免内存泄漏:及时解除无用对象的引用,加速类卸载。