1.加载阶段
在Java类的生命周期中,**加载阶段(Loading)**是类生命周期的第一个阶段,指的是JVM将类的字节码文件从外部(通常是硬盘或其他存储介质)加载到内存中的过程。加载阶段涉及到类加载器(ClassLoader)从类路径中找到相应的类文件,并将其加载到JVM内存中以便后续使用。
加载阶段的主要步骤:
-
请求加载:
- 当JVM遇到一个尚未加载的类时(例如通过代码中的
new
关键字、方法调用等),它会请求类加载器来加载该类。 - 例如,在程序执行到某个地方需要使用
Example
类时,JVM会通过类加载器请求加载Example
类。
- 当JVM遇到一个尚未加载的类时(例如通过代码中的
-
查找类文件:
- 类加载器通过类路径(classpath)查找指定的类文件。在
classpath
中,类加载器会查找与该类名相对应的.class
文件。 - 如果类是在JAR文件或其他包中,类加载器也会搜索JAR文件或其他包中的相应类。
- 类加载器通过类路径(classpath)查找指定的类文件。在
-
加载类字节码:
- 一旦类加载器找到了相应的
.class
文件,它就会将该类的字节码加载到JVM的内存中。 - 类的字节码是二进制格式的,它包含了类的结构信息,如类的字段、方法、常量池等。
- 一旦类加载器找到了相应的
-
创建类的
Class
对象:- 在字节码加载到内存后,JVM会为该类创建一个
Class
对象。这个Class
对象包含了类的元数据,例如类的名称、父类、实现的接口、方法、字段等。 - 通过
Class
对象,程序可以在运行时动态访问类的信息(反射机制)。
- 在字节码加载到内存后,JVM会为该类创建一个
类加载的过程可以分为以下几个具体的步骤:
1. 加载类的字节码
- 类加载器:JVM使用类加载器来加载类。类加载器从类路径中查找并读取类的字节码,通常会有不同类型的类加载器,如:
- Bootstrap ClassLoader:负责加载JDK的核心库(如
java.lang.*
)。 - Extension ClassLoader:负责加载JDK扩展库中的类。
- System ClassLoader:负责加载用户程序中的类(通常是应用程序的类路径)。
- Bootstrap ClassLoader:负责加载JDK的核心库(如
- 如果类加载器没有找到类,它会抛出
ClassNotFoundException
。
2. 加载类的元数据
- 一旦字节码被加载到内存,JVM会解析并存储类的元数据,包括字段、方法、常量池、接口、父类等信息。此时,类的信息存储在JVM的方法区(在JDK8之前是永久代,JDK8以后是元空间)中。
3. 分配内存
- JVM为类的字段、方法等分配内存空间,准备在后续的连接阶段使用。
- 在此阶段,类的静态变量并不会被初始化。静态变量的初始化将在后续的初始化阶段进行。
4. 返回Class对象
- 每个被加载的类都有一个
Class
对象,Class
对象是Java反射机制的一部分。通过这个对象,可以在运行时访问类的结构、字段、方法等信息。
加载阶段与其他阶段的关系:
- 加载阶段与后续的验证(Verification)、准备(Preparation)、解析(Resolution)等阶段不同,它只是单纯地将类的字节码加载到JVM内存中。类的结构会在后续的连接阶段和初始化阶段中进一步处理。
类加载器的作用:
类加载器的作用非常重要,它负责从类路径中查找并加载类文件。在加载类时,JVM会根据类加载器的机制来决定如何加载不同的类。类加载器有几种类型,它们的职责如下:
-
引导类加载器(Bootstrap ClassLoader):
- 加载JVM的核心类库,如
rt.jar
中的类(例如java.lang.Object
、java.util.*
等)。 - 它由C++编写,无法直接使用Java编写。
- 加载JVM的核心类库,如
-
扩展类加载器(Extension ClassLoader):
- 加载JDK的扩展类库,如
jre/lib/ext
目录下的类。
- 加载JDK的扩展类库,如
-
系统类加载器(System ClassLoader):
- 加载应用程序类路径(
classpath
)下的类。
- 加载应用程序类路径(
-
自定义类加载器:
- 用户可以根据需要自己实现类加载器,提供特殊的类加载方式,如从网络加载类、从数据库加载类等。
加载阶段小结:
- 加载阶段的关键任务是将类的字节码从外部(如磁盘)加载到JVM的内存中。
- 加载阶段是类生命周期的起点,涉及到类加载器的工作、字节码的读取和类的
Class
对象的创建。 - 类加载是延迟的,只有在类被第一次引用时,JVM才会加载类。
2.连接阶段
在Java类的生命周期中,**连接阶段(Linking)是继加载(Loading)**之后的第二个阶段,它包括三个重要的子阶段:验证(Verification)、准备(Preparation)和解析(Resolution)。这个阶段的目的是确保类的字节码是合法的并准备好可以在JVM中使用。
连接阶段的三个子阶段:
1. 验证(Verification)
- 验证是对加载的字节码进行合法性检查,确保它符合Java虚拟机规范,不会对JVM的稳定性和安全性构成威胁。
- 验证阶段主要有以下几个方面:
- 文件格式验证:检查字节码是否符合Java虚拟机的规范(如魔数
0xCAFEBABE
、常量池的有效性等)。 - 结构验证:确保类的内部结构(如字段、方法等)是合法的,符合JVM的规范。
- 类型验证:检查类的方法、字段等的数据类型是否正确,是否有可能导致类型转换错误。
- 符号引用验证:确保类中使用的符号引用能够在类加载过程中正确地解析。
- 文件格式验证:检查字节码是否符合Java虚拟机的规范(如魔数
这个阶段的目的是防止恶意的字节码(例如,由反编译工具或恶意修改的字节码)破坏JVM的安全性或导致运行时错误。
2. 准备(Preparation)
- 准备阶段是为类的静态变量分配内存并赋予默认值的过程。此时并不会执行类的初始化操作(即静态变量不会被赋予实际的初值),只是简单地为静态变量分配内存并赋予默认值。
- 静态变量的默认值:
- 基本数据类型(如
int
、float
)会被赋予其类型的默认值(例如int
的默认值是0
,boolean
的默认值是false
)。 - 对象引用类型(如
String
)会被赋值为null
。 - 对于数组,所有元素的默认值也是类型的默认值。
- static final 修饰成员变量会被赋予实际值
- 基本数据类型(如
需要注意的是,这一阶段并不会初始化静态变量的实际值,而只是为它们分配内存。静态变量的实际初始化会在初始化阶段发生。
3. 解析(Resolution)
- 解析是将类中的符号引用转换为实际的内存地址或直接引用的过程。
- 符号引用是类、方法、字段等在字节码中的表示,它们在类加载时会被引用解析为实际的内存地址,确保JVM能够找到实际的代码和数据。
- 类的解析:类的符号引用被转换为内存中的类对象。
- 字段的解析:字段的符号引用被解析为字段的内存位置。
- 方法的解析:方法的符号引用被解析为实际的方法地址。
解析过程中,JVM会遍历类中的所有符号引用,确保它们在内存中能找到对应的对象、方法或字段。如果某个符号引用无法解析(比如引用的类或方法不存在),就会抛出ClassNotFoundException
或NoSuchMethodException
等异常。
连接阶段小结
连接阶段的目的是将类加载到JVM后,确保它是合法的,并准备好类的静态信息和符号引用。这个阶段包括三个子阶段:
- 验证:检查字节码是否符合JVM的规范,确保没有安全漏洞。
- 准备:为类的静态变量分配内存,并赋予默认值。
- 解析:将类中的符号引用(类、字段、方法)转换为实际的内存地址。
这三个阶段为类的初始化和执行做了充分的准备,确保了类在运行时的正确性和效率。
3.初始化阶段
在Java类的生命周期中,初始化阶段是继加载阶段和连接阶段(包括验证、准备、解析)之后的最后一个阶段,主要涉及类的静态变量的初始化和类的初始化块(如果存在)执行的过程。
初始化阶段的关键点:
- 静态变量初始化:
- 在初始化阶段,JVM会为类的静态变量赋值。静态变量是属于类本身的,而不是类的实例,它们在类的加载时就被分配内存,并且会被赋予默认值(如
0
、false
、null
等)。但是,它们的实际初始值是由静态初始化块或类构造器中的代码赋予的。
- 在初始化阶段,JVM会为类的静态变量赋值。静态变量是属于类本身的,而不是类的实例,它们在类的加载时就被分配内存,并且会被赋予默认值(如
- 静态初始化块执行:
- 如果类有静态初始化块(
static {}
),JVM会在初始化阶段执行这些静态代码块。静态代码块只会在类加载时执行一次。
- 如果类有静态初始化块(
- 类初始化:
- 类的初始化是通过类初始化过程来完成的,这个过程通常会在第一次访问类的静态成员(字段、方法)时触发,或者在实例化类时触发。
初始化阶段的详细过程:
-
静态变量初始化:
- 在类的初始化阶段,JVM会根据类的字节码为静态字段分配内存,并为这些字段赋值。
- 如果某个静态字段没有显式赋值,它会被赋予类型的默认值(例如,
0
、false
、null
等)。
-
执行静态初始化块(
static {}
):- 如果类中定义了静态代码块(
static
关键字修饰的代码块),JVM会在类加载之后,初始化之前执行这些代码。 - 静态代码块通常用于初始化静态变量、进行资源配置、日志初始化等操作。
- 这些静态初始化块只会在类加载时执行一次。
例如:
public class MyClass { static int count; static { count = 10; System.out.println("Static block executed. count = " + count); } public static void main(String[] args) { System.out.println("Main method executed."); } }
输出结果:
Static block executed. count = 10 Main method executed.
- 在上面的例子中,静态块在类初始化时执行,所以在
main
方法输出之前,静态块已经被执行。
- 如果类中定义了静态代码块(
-
类初始化触发条件:
类的初始化并不会在类加载时立即发生,而是由以下几种情况触发:- 调用类的
static
代码块:在类的静态代码块中进行初始化时,会触发类的初始化。 - 创建类的实例:当创建类的对象时,如果类还没有初始化,JVM会初始化该类。
- 首次访问类的静态成员(字段或方法):类的初始化会在第一次引用静态字段或调用静态方法时触发。(注意:如果调用的是用static final 修饰的成员,则不会触发类的初始化)
- 调用类的
-
静态字段赋值和静态初始化块执行顺序:
- 如果类有静态变量的显式赋值以及静态初始化块,JVM会按照静态变量赋值和静态代码块执行的顺序依次进行初始化。
例如:
public class Test { static int x = 5; static int y; static { y = 10; System.out.println("Static block executed. y = " + y); } public static void main(String[] args) { System.out.println("Main method executed. x = " + x); } }
输出结果:
Static block executed. y = 10 Main method executed. x = 5
- 在上面的代码中,
x
和y
是静态变量,x
会在声明时被赋值为5
,而y
在静态块中被赋值为10
,静态块在类初始化时被执行。
静态初始化的触发时机:
类的初始化只有在以下几种情况下才会触发:
- 第一次引用该类的静态成员(字段、方法)。
- 第一次创建类的实例(如通过
new
关键字)。 - 调用
Class.forName()
加载类:如果Class.forName("ClassName")
被调用,JVM会加载并初始化类。 - 直接调用类的静态方法或字段:例如
MyClass.main()
,或者MyClass.someStaticMethod()
。
初始化阶段的总结:
- 静态变量赋值:在类的初始化阶段,静态变量会被赋值。如果没有显式赋值,JVM会给它们默认值(例如
0
、false
等)。 - 静态初始化块执行:类中如果有静态初始化块,JVM会在类初始化时执行这些静态代码块。静态代码块通常用于初始化类的静态资源。
- 触发时机:类的初始化并不会在类加载时立即发生,而是在类的静态成员首次被访问、类的实例化或者通过反射调用类时触发。
初始化阶段的目标是确保类的静态字段正确初始化并执行静态块中的代码,准备好类的运行环境。