类加载器(ClassLoader)会在多种情况下加载一个类,主要分为两大类:主动使用 和 被动使用。JVM 规范明确规定了类的主动使用场景,在这些场景下,类加载器会触发类的加载、链接和初始化。
1. 主动使用 (Active Use):
当 JVM 遇到以下情况时,会主动使用一个类,触发类加载器加载该类:
创建类的实例:
- 使用
new
关键字创建类的实例。 - 通过反射创建类的实例(
Class.forName()
,clazz.newInstance()
,constructor.newInstance()
)。
MyClass obj = new MyClass(); // 主动使用 MyClass Class<?> clazz = Class.forName("com.example.MyClass"); // 主动使用 MyClass MyClass obj2 = (MyClass) clazz.newInstance(); // 主动使用 MyClass
- 使用
访问类的静态变量 (static field):
- 读取或设置类的静态变量(
getstatic
或putstatic
字节码指令),除了static final
修饰的编译时常量。
* 编译时常量:static final
修饰的基本类型或字符串字面量,在编译时值已确定,不会触发类的初始化(直接从常量池中获取)。
* 运行时常量:static final
修饰,但值需要在运行时才能确定,会触发类的初始化。
int value = MyClass.staticField; // 主动使用 MyClass (读取静态变量) MyClass.staticField = 10; // 主动使用 MyClass (设置静态变量) // finalString 在编译期就能确定值, 直接从常量池获取, 不会触发 MyClass 初始化 String str = MyClass.finalString; // finalValue 需要在运行时调用方法才能确定值, 会触发 MyClass 初始化 long time = MyClass.finalValue;
- 读取或设置类的静态变量(
调用类的静态方法 (static method):
- 使用
invokestatic
字节码指令调用类的静态方法。
MyClass.staticMethod(); // 主动使用 MyClass
- 使用
反射 (Reflection):
- 使用
java.lang.reflect
包中的方法对类进行反射操作。
Class<?> clazz = Class.forName("com.example.MyClass"); // 主动使用 MyClass Method method = clazz.getMethod("myMethod"); // 主动使用 MyClass Field field = clazz.getField("myField"); // 主动使用 MyClass
- 使用
初始化类的子类:
- 当初始化一个类时,如果其父类还没有被初始化,则会先初始化其父类(接口除外)。
- 注意:初始化一个类的子类, 并不会触发该类所实现接口的初始化.
// 假设 SubClass 是 MyClass 的子类 SubClass obj = new SubClass(); // 会先触发 MyClass 的初始化,再触发 SubClass 的初始化
Java 虚拟机启动时的主类:
- 当 Java 虚拟机启动时,会先初始化包含
main
方法的主类。
// 运行 java MyMainClass public class MyMainClass { public static void main(String[] args) { // ... } } // 会触发 MyMainClass 的初始化
- 当 Java 虚拟机启动时,会先初始化包含
接口的初始化:
- 接口中定义了
default
方法 (JDK8及以后). - 如果一个接口的实现类被初始化, 则该接口也会被初始化.
- 接口中定义了
JDK 1.7+ 的动态语言支持:
- 如果一个
java.lang.invoke.MethodHandle
实例的解析结果为REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 如果一个
* 使用 `invokedynamic` 指令.
2. 被动使用 (Passive Use):
- 定义:
- 除了上述主动使用的情况外,其他引用类的方式都不会触发类的初始化,称为被动使用。
- 被动使用不会执行类的
<clinit>()
方法(即不会执行静态变量赋值和静态代码块)。
- 常见场景:
通过子类引用父类的静态字段: 只会触发父类的初始化,不会触发子类的初始化。
class SuperClass { static int value = 10; static { System.out.println("SuperClass initialized"); } } class SubClass extends SuperClass { static { System.out.println("SubClass initialized"); } } public class PassiveReference { public static void main(String[] args) { System.out.println(SubClass.value); // 只会输出 "SuperClass initialized" 和 10 } }
定义类类型的数组: 不会触发类的初始化。
MyClass[] arr = new MyClass[10]; // 不会触发 MyClass 的初始化
引用类的常量 (编译时常量): 不会触发类的初始化(常量在编译阶段已存入常量池)。
class MyClass{ public static final int CONSTANT_VALUE = 10; //编译时常量 } public class Test{ public static void main(String[] args){ System.out.println(MyClass.CONSTANT_VALUE); //不会触发MyClass初始化 } }
- 通过类加载器加载类,但不进行初始化:
ClassLoader.loadClass()
方法默认只加载类,不进行初始化。- 要触发类的初始化,需要调用
Class.forName(className, true, classLoader)
,并将第二个参数设置为true
。
ClassLoader classLoader = ClassLoader.getSystemClassLoader(); // 只加载,不初始化 Class<?> clazz1 = classLoader.loadClass("com.example.MyClass"); // 加载并初始化 Class<?> clazz2 = Class.forName("com.example.MyClass", true, classLoader);
总结:
- 类加载器会在遇到主动使用类的情况时加载类,包括创建实例、访问静态成员、反射、初始化子类、启动主类等。
- 被动使用类不会触发类的初始化,例如通过子类引用父类的静态字段、定义类类型的数组、引用编译时常量等。
ClassLoader.loadClass()
默认只加载类,不进行初始化。要加载并初始化类,可以使用Class.forName()
。- 了解类加载的时机我助于我们排查类加载相关的错误(例如
NoClassDefFoundError
、ClassNotFoundException
)、优化程序性能(例如延迟加载)以及实现一些高级功能(例如热部署)