在上一篇博文中,我们对JVM的整体架构进行了概览,包括类加载器子系统、运行时数据区、执行引擎、本地方法接口(JNI)以及本地方法库的作用。本文将深入探讨类加载器子系统中的类加载过程。
1 类加载过程
类的生命周期如下,其中加载的过程包括了加载、验证、准备、解析、初始化五个阶段
- 加载:类在使用时,会通过全限定类名从磁盘查找并读入字节码文件,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。例如调用类的main()方法,new对象等等,在加载阶段会在堆内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
- 验证:校验字节码文件的正确性,确保其符合JVM规范,没有安全问题。验证过程包括文件格式验证、元数据验证、字节码验证和符号引用验证。
- 准备:给类的静态变量分配内存,并赋予默认值。例如,对于整型变量,其默认值会被初始化为0。
- 解析:这个阶段涉及到将类中的符号引用替换为直接引用。符号引用是类、接口、字段和方法的名称,而直接引用是指向目标的实际内存地址或指针。解析过程包括静态链接和动态链接,静态链接在类加载期间完成,而动态链接在程序运行期间完成。
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块(
<clinit>
方法)
2 类的半初始化问题
类的半初始化问题通常发生在加载阶段和初始化阶段之间。在准备阶段,静态变量已经被分配内存并初始化为默认值,但是在初始化阶段执行之前,静态变量还没有赋予最终的初始值。
这意味着,在准备阶段之后,如果一个类引用了另一个类的静态变量,而这个静态变量还没有被赋予最终的初始值,就会导致类的半初始化问题。
class Apple {
static Apple apple = new Apple(10);
static double price = 20.00;
double totalpay;
public Apple (double discount) {
System.out.println("===="+price);
totalpay = price - discount;
}
}
public class PriceTest01 {
public static void main(String[] args) {
System.out.println(Apple.apple.totalpay);
}
}
运行结果:
====0.0
-10.0
其中Apple.apple访问了类的静态变量,会触发类的初始化,即加载->连接->初始化。
连接结束后,Apple类的apple属性为null,price为0,开始进行类的初始化,即一开始先给静态属性apple属性赋值。当执行构造函数时,price此时还未初始化完成,其值为默认值 0,这时构造函数的 price 就是0,所以最终打印出来的结果是-10 而不是 10。
只需将静态属性price和apple的位置对调,确保在执行apple构造函数前price已初始化完成即可。
class Apple{
static double price = 20.00;
static Apple apple = new Apple(10);
double totalpay;
public Apple (double discount) {
System.out.println("===="+price);
totalpay = price - discount;
}
}
public class PriceTest01 {
public static void main(String[] args) {
System.out.println(Apple.apple.totalpay);
}
}
运行结果:
====20.0
10.0
3 总结
本文介绍了类加载器子系统中的类加载过程,以及常说的类的半初始化问题。类加载是Java程序运行的基础,包括加载、验证、准备、解析和初始化五个阶段。了解这些阶段以及如何避免类的半初始化问题对于开发高效、稳定的Java应用程序至关重要。
在后续的博文中,我们将继续深入类加载器子系统,探讨类加载器的层次结构和双亲委派模型等话题。