本文主要记录下类的加载过程,一个类从加载到内存中开始到被卸载的整个生命周期作为java程序员来说应该还是要清楚的。
类的生命周期
类从加载到内存中到卸载的整个过程中经历了如下的过程:
这几个步骤中 验证,准备,解析这三个步骤有称为连接阶段,大体的顺序是加载,验证,准备,解析,初始化,使用和卸载,前四个有部分有交叉顺序。
类的加载时机
类加载的时机,也就是类初始化的时机(加载,验证,准备,解析)。
遇到 new ,getstatic,putstatic和invokestatic这4条指令的时候,也就是通过new关键字实例化对象,读取或者设置一个静态变量以及调用静态方法。
反射调用的时候如果没有初始化就会加载该类。
初始化子类的时候发现父类还没有被初始化就会先初始化父类。
虚拟机启动的时候,会初始化主类(含有main方法的类)
当使用JDK1.7及以上的版本中的动态语言支持时,若一个java.lang.invoke.MethodHandle实例最后的解析结果是:REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先出发它的初始化过程。
虚拟机规范中指出有且只有这5种场景会出发初始化,并且这5种场景的行为称为对一个类的“主动引用”,除此之外所有引用类的方式都不会触发初始化,不触发初始化的也被称为被动引用。
被动引用的例子
通过子类引用父类的静态变量不会初始化子类
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 666;
public static final String JVM_TEST = "JVM TEST";
}
/**
* 子类
*/
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
/**
* 测试
* @author 波波烤鸭
* @email [email protected]
*
*/
public class Test {
public static void main(String[] args){
System.out.println(SubClass.value);
}
}
输出结果
SuperClass init!
666
通过数组定义来引用类,不会触发类的初始化
案例直接用1中的类结构
/**
* 测试
* @author 波波烤鸭
* @email [email protected]
*
*/
public class Test {
public static void main(String[] args){
SuperClass[] supers = new SuperClass[10];
}
}
运行结果并没有打印出“SuperClass init!”,这说明并没有对SuperClass进行初始化,定义数组不会触发类的初始化
常量在编译阶段会被存入调用类的常量池中,本质上并没有直接引用到定义常量的类,所以这种场景也不会触发类的初始化
public class Test {
public static void main(String[] args){
// SuperClass[] supers = new SuperClass[12];
System.out.println(SuperClass.JVM_TEST);
}
}
运行结果也没有打印出“SuperClass init!”,因为虽然引用了SuperClass的常量,但其实在编译极端通过常量传播优化,已经将此常量存储到了Test类的常量池中,因Test类对此常量的引用,都会转化为Test类对自身常量池的引用了。这说明SuperClass和Test这两个类,在编译阶段完成后就没有任何关系了。
接口的加载过程和类的加载过程步骤上是一致的,但是稍有不同的是上面的例子都是用静态语句块“static{}”来输出初始化信息的,在接口中不能使用“static{}”静态语句块。还有一个不同是:当一个类在初始化的时候,要求其父类全部都已经初始化过了,但是一个接口在初始化的时候,不要求其父接口都初始化过,只有真正使用到父接口的时候(例如:引用父接口中定义的常量)才会初始化。
参考《深入Java虚拟机》