JVM 不和包括 Java 在内的任何语言绑定,它只与 "Class文件" 这种特定的二进制文件格式所关联。而 Class 文件也并非只能通过 Java 源文件编译生成,可以通过如下途径而来:
JVM 把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这个过程被称为虚拟机的「类加载机制」。
Class 文件中描述的关于类的信息最终要加载到 JVM 中才能被运行和使用。
1. 类加载的时机
1.1 类的生命周期
一个类型(类或接口)从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期会经历加载(Loading)、验证(Verification)、准备(Prepare)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析统称为连接(Linking)。如图所示:
1.2 初始化时机
JVM 规范对于“加载”阶段并未强制约束。但对于“初始化”阶段,则规定有且仅有以下六种情况必须立即对其“初始化”:
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时。场景如下:
- 使用 new 关键字实例化对象;
- 读/写静态字段(static 修饰,无 final);
- 调用静态方法。
- 使用
java.lang.reflect
的方法对类型进行反射调用时。 - 初始化类时,若父类尚未初始化,需要先初始化其父类。
- 虚拟机启动时,需要先初始化用户指定的主类(main 方法所在类)。
- 使用 JDK 7 新加入的动态语言支持时,若一个 java.lang.invoke.MethodHandle 实例最后解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,且该方法句柄对应的类未初始化,需要先初始化【平时似乎没用到过,暂不深究,以后有机会再分析】。
- 接口中定义了 JDK 8 加入的默认方法(default 修饰)时,在该接口的实现类初始化之前,需要先初始化这个接口。
注意:当一个“类”在初始化时,要求其父类全都已经初始化;但是,一个“接口”在初始化时,并不要求父接口全都初始化,只有真正使用到父接口时才会初始化(比如引用接口定义的常量)。
1.3 主动引用&被动引用
上述六种情况的行为称为对一个类型的“主动引用”,而除此之外的其他所有引用类型方式都不会触发初始化,称为“被动引用”。被动引用举例如下:
- 示例代码
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
public static final String HELLO_WORLD = "hello, world";
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
PS: 为了跟踪类加载信息,可配置虚拟机参数
-XX:+TraceClassLoading
。
- eg1
/**
* 通过子类引用父类的静态字段,不会导致子类初始化
*/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
/* 类加载情况:SubClass 和 SuperClass 均被加载
*
* 输出结果(父类初始化,子类未初始化):
* SupClass init!
* 123
*/
- eg2
/**
* 通过数组定义来引用类,不会触发此类的初始化
*/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] superClasses = new SuperClass[10];
}
}
/* 类加载情况:SuperClass 被加载
* 输出结果为空,SuperClass 未初始化
*/
- eg3
/**
* 常量在【编译阶段】会存入调用类(NotInitialization)的常量池中,
* 本质上并没有直接引用到定义常量的类,因此不会触发其初始化
*/
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SuperClass.HELLO_WORLD);
}
}
/* 类加载情况:SubClass 和 SuperClass 均未被加载
*
* 输出结果:
* hello, world
*/
编译阶段通过常量传播优化,已将该常量的值("hello, world")直接存储在 NotInitialization 类的常量池中,以后 NotInitialization 对常量 SuperClass.HELLO_WORLD 的引用实际都被转化为对自身常量池的引用了。
PS: 其实 NotInitialization 类的 Class 文件中并不存在 SuperClass 类的符号引用入口,这两个类在编译成 Class 文件之后就没联系了。<