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

类加载器(ClassLoader)会在多种情况下加载一个类,主要分为两大类:主动使用被动使用。JVM 规范明确规定了类的主动使用场景,在这些场景下,类加载器会触发类的加载、链接和初始化。

1. 主动使用 (Active Use):

当 JVM 遇到以下情况时,会主动使用一个类,触发类加载器加载该类:

  • 创建类的实例:

    • 使用 new 关键字创建类的实例。
    • 通过反射创建类的实例(Class.forName(), clazz.newInstance(), constructor.newInstance())。
    java 复制代码
    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修饰,但值需要在运行时才能确定,会触发类的初始化。
    java 复制代码
    int value = MyClass.staticField; // 主动使用 MyClass (读取静态变量)
    MyClass.staticField = 10;     // 主动使用 MyClass (设置静态变量)
    
    // finalString 在编译期就能确定值, 直接从常量池获取, 不会触发 MyClass 初始化
    String str = MyClass.finalString;
    
    // finalValue 需要在运行时调用方法才能确定值, 会触发 MyClass 初始化
    long time = MyClass.finalValue;
  • 调用类的静态方法 (static method):

    • 使用 invokestatic 字节码指令调用类的静态方法。
    java 复制代码
    MyClass.staticMethod(); // 主动使用 MyClass
  • 反射 (Reflection):

    • 使用 java.lang.reflect 包中的方法对类进行反射操作。
    java 复制代码
    Class<?> clazz = Class.forName("com.example.MyClass"); // 主动使用 MyClass
    Method method = clazz.getMethod("myMethod"); // 主动使用 MyClass
    Field field = clazz.getField("myField");    // 主动使用 MyClass
  • 初始化类的子类:

    • 当初始化一个类时,如果其父类还没有被初始化,则会先初始化其父类(接口除外)。
    • 注意:初始化一个类的子类, 并不会触发该类所实现接口的初始化.
    java 复制代码
    // 假设 SubClass 是 MyClass 的子类
    SubClass obj = new SubClass(); // 会先触发 MyClass 的初始化,再触发 SubClass 的初始化
  • Java 虚拟机启动时的主类:

    • 当 Java 虚拟机启动时,会先初始化包含 main 方法的主类。
    java 复制代码
    // 运行 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>() 方法(即不会执行静态变量赋值和静态代码块)。
  • 常见场景:

    • 通过子类引用父类的静态字段: 只会触发父类的初始化,不会触发子类的初始化。

      java 复制代码
      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
          }
      }
    • 定义类类型的数组: 不会触发类的初始化。

      java 复制代码
      MyClass[] arr = new MyClass[10]; // 不会触发 MyClass 的初始化
    • 引用类的常量 (编译时常量): 不会触发类的初始化(常量在编译阶段已存入常量池)。

    java 复制代码
      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
      java 复制代码
       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)、优化程序性能(例如延迟加载)以及实现一些高级功能(例如热部署)
相关推荐
合作小小程序员小小店17 小时前
web开发,在线%车辆管理%系统,基于Idea,html,css,vue,java,springboot,mysql
java·spring boot·vscode·html5·web app
龙茶清欢17 小时前
在 Spring Cloud Gateway 中实现跨域(CORS)的两种主要方式
java·spring boot·spring cloud·微服务·gateway
1710orange17 小时前
java设计模式:工厂方法 + 建造者模式
java·设计模式
我不是混子18 小时前
什么是Java 的 Lambda 表达式?
java·后端
小蝙蝠侠18 小时前
JMeter 执行流程
java·jmeter
程序员小假19 小时前
我们来说一说 ThreadLocal 内存泄漏
java·后端
xq952719 小时前
获取Facebook 散列利器 来了 十六进制到 Base64 转换器
java
我不是混子19 小时前
聊聊Spring事件机制
java·后端
DKPT19 小时前
JVM栈溢出时如何dump栈信息?
java·jvm·笔记·学习·spring
DKPT19 小时前
JVM堆大小如何设置?
java·开发语言·jvm·笔记·学习