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 虚拟机》