天天看点

JVM中类加载的时机

 本文主要记录下类的加载过程,一个类从加载到内存中开始到被卸载的整个生命周期作为java程序员来说应该还是要清楚的。

类的生命周期

 类从加载到内存中到卸载的整个过程中经历了如下的过程:

JVM中类加载的时机

 这几个步骤中 验证,准备,解析这三个步骤有称为连接阶段,大体的顺序是加载,验证,准备,解析,初始化,使用和卸载,前四个有部分有交叉顺序。

类的加载时机

 类加载的时机,也就是类初始化的时机(加载,验证,准备,解析)。

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