在 Java 虚拟机 (JVM) 中,类的初始化是程序执行的关键步骤之一。理解颜群JVM【03】类的初始化过程对于避免各种潜在的运行时错误至关重要。本文将深入探讨 JVM 类初始化的底层原理,并通过具体的代码示例和实战经验,帮助开发者更好地掌握这一核心概念。
类加载过程与初始化时机
类的加载过程包括加载、链接(验证、准备、解析)和初始化五个阶段。初始化阶段是类加载的最后一步,也是执行类构造器 <clinit>() 方法的过程。该方法由编译器自动收集类中所有静态变量的赋值动作和静态语句块中的语句合并产生。
触发类初始化的五种场景
以下五种场景会触发类的初始化:
- new 关键字创建实例:使用
new关键字创建类的实例对象。 - 访问类的静态成员:访问类的静态字段(除final修饰的常量)或静态方法。
- 反射调用:使用
java.lang.reflect包中的方法对类进行反射调用。 - 初始化子类:当初始化一个类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 虚拟机启动时指定的启动类:当虚拟机启动时,用户需要指定一个要执行的主类 (包含 main() 方法的类),虚拟机会先初始化这个主类。
代码示例:验证类初始化时机
class Parent {
static {
System.out.println("Parent class initializing");
}
public static int value = 10;
}
class Child extends Parent {
static {
System.out.println("Child class initializing");
}
public static int childValue = 20;
}
public class ClassInitialization {
public static void main(String[] args) {
System.out.println(Child.value); // 访问父类的静态成员,触发父类初始化
}
}
上述代码只会输出 Parent class initializing 和 10,而不会输出 Child class initializing。这是因为访问的是父类的静态成员,只会触发父类的初始化,而不会触发子类的初始化。如果改为 System.out.println(Child.childValue); 则会先输出 Parent class initializing 和 Child class initializing,再输出 20。
<clinit>() 方法的线程安全性
虚拟机会保证一个类的 <clinit>() 方法在多线程环境中被正确的加锁、同步。这意味着如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 <clinit>() 方法,其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 方法完毕。这种机制确保了类的初始化过程是线程安全的。
代码示例:多线程环境下的类初始化
class Singleton {
private static Singleton instance;
private Singleton() {
System.out.println("Singleton initializing");
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton(); // 线程安全由JVM保证
}
return instance;
}
}
public class SingletonTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
Singleton.getInstance();
}).start();
}
}
}
在上述代码中,虽然多个线程同时调用 Singleton.getInstance() 方法,但是只会有一个线程会执行 Singleton 类的初始化过程,并创建唯一的 Singleton 实例。 其他线程会被阻塞,直到第一个线程完成初始化。这就是 <clinit>() 方法线程安全性的体现。
实战避坑经验总结
- 避免循环依赖导致的死锁:在静态初始化块中,尽量避免对其他类的静态成员进行引用,防止出现循环依赖,导致死锁。
- 注意 final 静态变量的初始化:final 静态变量必须在声明时或者静态初始化块中进行初始化,否则会编译报错。
- 理解类加载器的作用:不同的类加载器加载的类是相互隔离的,即使是同一个类,如果由不同的类加载器加载,也会被认为是不同的类。
- 利用延迟初始化提升性能:如果一个类的初始化过程比较耗时,可以考虑使用延迟初始化,只有在真正需要使用该类的时候才进行初始化。例如,可以使用
Holder模式来实现延迟初始化。
深入理解 JVM 类初始化,提升代码质量
掌握 JVM 类初始化的机制,能够帮助我们编写出更加健壮、高效的 Java 代码。希望本文能够帮助读者更深入地理解颜群JVM【03】类的初始化,并在实际开发中避免常见的坑,提升代码质量。理解 JVM 的类加载机制,能够更好地理解 Spring 框架的 Bean 加载过程、Tomcat 服务器的类隔离机制,以及 Dubbo 框架的 SPI 扩展机制。这些高级框架和技术都依赖于 JVM 的类加载和初始化机制。
冠军资讯
代码一只喵