【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言:
在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。
目录
编辑前言:
类的生命周期
类的加载阶段
核心任务
连接阶段
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化阶段
不会导致初始化
初始化触发条件(严格规定)
使用阶段
卸载阶段
面试题
总结
类的生命周期
一个类的生命周期分为五个阶段,分别是加载,连接,初始化,使用和卸载。
类的加载阶段
加载是将类的字节码文件(.class)加载到 JVM 内存的过程,类加载器会通过不同的形式,去得到字节码文件的内容,然后通过JVM,把内容加载到方法区和堆区,而我们开发者只能访问到堆区的内容,方法区我们是访问不到的,堆区的内容是字节码文件的核心内容。
核心任务
-
查找字节码:
-
通过类的全限定名查找字节码文件
-
搜索路径:类路径(classpath)、JAR 文件、网络资源等
-
-
创建类结构:
-
解析字节码并创建对应的类数据结构
-
在方法区(元空间)存储类的运行时常量池、字段和方法信息
-
-
创建 Class 对象:
-
在堆内存中创建 java.lang.Class 对象
-
该对象作为访问类元数据的入口
-
连接阶段
在加载阶段结束后进入连接阶段,连接阶段分为三个子阶段,确保类字节码的正确性和可用性
验证(Verification)
确保字节码符合 JVM 规范和安全要求:
-
文件格式验证:魔数(0xCAFEBABE)、版本号等
-
元数据验证:语义检查(是否有父类、final类是否被继承等)
-
字节码验证:数据流和控制流分析
-
符号引用验证:检查引用的类、字段和方法是否存在
准备(Preparation)
为类变量分配内存并设置初始值:
-
静态变量分配:
-
在方法区分配内存
-
设置类型默认值(零值)
-
int → 0
-
boolean → false
-
引用类型 → null
-
-
-
特殊处理:
-
static final 常量:直接赋程序指定值
-
// 准备阶段直接赋值
public static final int MAX = 100;
解析(Resolution)
将符号引用转换为直接引用:
-
符号引用:用一组符号描述引用的目标
-
直接引用:指向目标的指针、偏移量或句柄
初始化阶段
执行静态代码块的内容和给静态变量赋值,以字节码文件视角就是执行<clinit>方法
<clinit>()
方法特性
-
自动生成:
-
编译器收集所有类变量赋值和静态代码块
-
按源代码顺序合并生成
-
-
线程安全:
-
JVM 保证只有一个线程执行初始化
-
其他线程会阻塞等待完成
-
-
父类优先:
-
执行子类的
<clinit>()
前 -
必须先执行父类的
<clinit>()
-
不会导致初始化
- 无静态代码块且无静态变量赋值语句。
- 有静态变量的声明,但是没有赋值语句。
- 静态变量的定义使用final关键字,这类变量会在准备阶段直接初始化 。
- 直接访问父类的静态变量,不会触发子类的初始化。
- 数组的创建不会导致数组中元素的类进行初始化。
初始化触发条件(严格规定)
new
实例化对象访问类的静态变量(非 final 常量)
调用类的静态方法
反射调用(Class.forName())
初始化子类时(父类需先初始化)
JVM 启动时的主类
MethodHandle 解析结果涉及静态方法
使用阶段
类的初始化之后,它就可以在程序中自由使用了。这包括创建实例、调用方法和访问字段等操作。在这个阶段,对象会被创建和操作,它们各自也会经历自己的生命周期。
卸载阶段
在某些情况下,当一个类不再需要时,它会被卸载。类的卸载发生在垃圾收集的过程中,当确定某个类的Class对象不再被引用,且对应的ClassLoader实例也不再存在时,JVM就可能卸载这个类。但是,在常见的Java应用中,由于系统类加载器加载的类一直会被引用,所以这些类通常只有在JVM停止运行时才会被卸载。
面试题
说出一下代码的运行结果:
public class Demo01 {public static void main(String[] args) {System.out.println(B02.a);}
}
class A02
{static int a=0;static{System.out.println("A02");a=1;}
}
class B02 extends A02{static{System.out.println("B02");a=2;}
}
运行结果为A02 1 原因是因为:访问父类的静态变量,只初始化父类。
public class Demo01 {public static void main(String[] args) {new B02();System.out.println(B02.a);}
}
class A02
{static int a=0;static{System.out.println("A02");a=1;}
}
class B02 extends A02{static{System.out.println("B02");a=2;}
}
首先执行main方法,new了B02所以要初始化它,但是它是子类,所以要先初始化A02,首先给赋值为0然后打印出A02,在给a赋值1,在初始化B02,打印出B02,a赋值为2,然后打印出a的值2
运行结果是:A02 B02 2
public class test5 {public static void main(String[] args) {System.out.println("A");new test5();new test5();}public test5(){System.out.println("B");}{System.out.println("C");}static {System.out.println("D");}}
- 先执行test5这个类的静态代码块输出D
- 在执行main方法打印A
- 在初始化test5,执行构造器方法和代码块方法,但是代码块先执行所以第一个new执行CB
- 第二个new执行CB
- 结果:DACBCB
以上面试题你都做对了吗?
总结
Java类的生命周期包括:加载(将字节码加载到内存,生成Class对象)、连接(验证字节码、准备静态变量内存并赋默认值、解析符号引用)、初始化(执行静态代码块和赋值)、使用(创建实例、调用方法)、卸载(类不再使用时被回收)。按此顺序完成从加载到销毁的完整过程。
学习类的生命周期,能让我们对java语言有一个更深的认识,也能让我们在面试中多一点机会。
总之,类的生命周期从加载到卸载,经历了多个阶段,每个阶段都有特定的任务和目标。理解类的生命周期有助于我们更好地理解和管理Java程序的运行机制。
感谢你的阅读,希望文章能给你带来帮助,你的阅读点赞就是我最大的动力。