「JVM 执行子系统」类加载的时机

JVM 把类数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程称作 VM 的类加载;

Java 语言在前端编译阶段不会做连接,类的加载、连接、初始化都是在运行阶段完成的,这导致 Java 的提前编译比较困难;同时类加载时也增加了性能开销;但这却是 Java 应用提供高扩展性(动态扩展,运行期动态加载和动态连接)和灵活性的保障;

一个类型从被加载到 VM 内存,到卸载出内存,整个生命周期经历:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段;其中验证、准备、解析三部分统称为连接(Linking);

1. 主动引用

《Java 虚拟机规范》严格规定有且只有 6 种情况必须立即开始类的初始化(加载、验证、准备需在初始化开始前完成),即主动引用的场景;

  • 遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类型没有进行过初始化,需先触发初始化;
    • 使用 new 关键字实例化对象;
    • 读取或设置一个类型的静态字段(常量,即被 final 修饰、已在编译期把结果放入常量池的静态字段除外);
    • 调用一个类型的静态方法;
  • 使用 java.lang.reflect 包的方法对类型进行反射调用时,如果类型没有进行过初始化,需先触发初始化;
  • 初始化一个类时,发现其父类没有进行过初始化,需先触发其父类的初始化;
  • JVM 启动时,用户指定的一个要执行的主类(main 方法所在的类)需先触发初始化;
  • 使用 JDK 7 新加入的动态语言支持的 java.lang.invoke.MethodHandle 实例最后的解析结果是 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四中类型的方法句柄时,如果方法句柄对应的类没有进行过初始化,需先触发初始化;
  • 接口中定义了 JDK 8 新加入的默认方法(default 关键字修饰的接口方法),如果接口的实现类发生了初始化,该接口要在其之前初始化;

2. 被动引用

所有主动引用以外的引用方式(不会触发初始化)被称为被动引用;

被动引用示例

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
        // 执行结果:
        // SuperClass init!
        // 123
    }
}

/**
 * 被动使用类字段:
 * 通过子类引用父类的静态字段,不会导致子类初始化
 **/
class SuperClass {
    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

引用一个静态字段时,只有直接定义这个静态字段的类才会被初始化;

public class NotInitialization {
    /**
    * 被动使用类字段:
    * 通过数组定义来引用类,不会触发此类的初始化
    **/
    public static void main(String[] args) {
        SuperClass[] sca = new SuperClass[10];
    }
}

通过数组引用类时,不会直接触发 xxx.SuperClass 的初始化,而是触发一个 VM 自动生成的 Lxxx.SuperClass 类的初始化,创建动作由字节码 newarray 触发;

public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);
        // 执行结果:
        // hello world
    }
}

/**
 * 被动使用类字段:
 * 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
 **/
class ConstClass {
    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLOWORLD = "hello world";
}

在编译阶段存在常量传播优化,常量值 hello world 已直接存储在 NotInitialization 类的常量池中,此后 NotInitialization 类与 ConstClass 类再无关系;

3. 接口的加载时机

接口的加载与类加载的不同在于,类初始化时其父类必须已全部完成初始化,接口初始化时不要求父接口全部完成初始化,只有真正使用父接口(引用接口中的常量)才会触发其初始化;


上一篇:「JVM 执行子系统」字节码指令简介
下一篇:「JVM 执行子系统」类加载的 5 个阶段

PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!


参考资料:

  • [1]《深入理解 Java 虚拟机》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aurelius-Shu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值