一个类从被加载到JVM内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。
《Java虚拟机规范》中并没有强制规定何时需要开始类加载过程的第一个阶段“加载”,但是对于初始化阶段则严格规定有且仅有以下六种情况必须对类进行“初始化”:
- new、getstatic、putstatic或者invokestatic字节码指令,能生成这四条指令的场景有
● new实例化对象
● get/set一个类型的static字段(被final修饰除外)
● 调用static方法 - 反射调用
- 若初始化的类继承有父类,且父类还未被初始化,则父类必须先被初始化
- 启动时指定的主类,则该类会先被初始化
- 若java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newinvokeSpecial四种类型的方法句柄,且这些方法句柄对应的类还未初始化,则需要先被初始化
- 接口中有default关键字修饰的方法,改接口要在其实现类之前被初始化
以上六种场景称为对一个类型进行主动引用,其余的所有引用类型被称为被动引用,均不会发生初始化。
被动引用案例分析
案例一:字类引用父类的static字段
package org.ican.classloading.test1;
public class FatherClass {
public static int value = 20;
static {
System.out.println("FatherClass init");
}
}
public class ChildrenClass extends FatherClass {
static {
System.out.println("ChildrenClass init");
}
}
public class Test1 {
public static void main(String[] args) {
System.out.println(ChildrenClass.value);
}
}
上述代码运行后,输出为
FatherClass init
20
对于static字段,只有直接定义这个字段的类才会被初始化。
案例二:数组定义的引用类
package org.ican.classloading.test2;
public class ArrayItemClass {
public static int value = 20;
static {
System.out.println("ArrayItemClass init");
}
}
public class Test2 {
public static void main(String[] args) {
ArrayItemClass[] array = new ArrayItemClass[20];
}
}
上述代码运行后,未输出任何内容,数组定义的引用类,不会发生该类的初始化。
案例三:类的常量引用
package org.ican.classloading.test3;
public class ConstClass {
public final static String LOVE_WORLD = "love vani";
static {
System.out.println("ConstClass init");
}
}
public class Test3 {
public static void main(String[] args) {
System.out.println(ConstClass.LOVE_WORLD);
}
}
上述代码运行后,未输出任何内容,由于ConstClass中LOVE_WORLD字段为常量,在编译阶段进行了常量传播优化,改常量的值"love vani"直接存储在Test3类的常量池中,所以对ConstClass.LOVE_WORLD的引用,实际上只是对自身常量池的引用。