JVM 类加载器在什么情况下会加载一个类?

发布于:2025-04-02 ⋅ 阅读:(34) ⋅ 点赞:(0)

类加载器(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):

    • 读取或设置类的静态变量(getstaticputstatic 字节码指令),除了 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 的初始化
    
  • 接口的初始化:

    • 接口中定义了default方法 (JDK8及以后).
    • 如果一个接口的实现类被初始化, 则该接口也会被初始化.
  • JDK 1.7+ 的动态语言支持:

    • 如果一个 java.lang.invoke.MethodHandle 实例的解析结果为 REF_getStaticREF_putStaticREF_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()
  • 了解类加载的时机我助于我们排查类加载相关的错误(例如 NoClassDefFoundErrorClassNotFoundException)、优化程序性能(例如延迟加载)以及实现一些高级功能(例如热部署)

网站公告

今日签到

点亮在社区的每一天
去签到