类加载机制

定义

虚拟机把描述类的数据从Class文件中加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。

生命周期

类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括:加载、链接、初始化、使用、卸载,其中链接又包括验证、准备、解析。

类的初始化

虚拟机规范严格规定了有且仅有5种情况必须立即对类进行初始化:

  1. 遇到new,getstatic,putstatic或invokestatic这4条字节码指令时,如果类还没有进行初始化,则需要先触发其初始化。例如:使用new关键字实例化对象的时候、读取或设置一个类的静态属性的时候、以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类还没有进行初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化时,则需要先触发父类的初始化。
  4. 当虚拟机启动的时候,用户需要指定一个要执行的主类,虚拟机会先初始化这个类。
  5. 如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

这5种场景中的行为称为对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用,例如:

  • 通过子类引用父类的静态字段,不会导致子类初始化
public class Father {
    static {
        System.out.println("load father");
    }
    public static int value = 1;
}

class Child extends Father{
    static {
        System.out.println("load child");
    }
}
public static void main(String[] args) {
    System.out.println(Child.value);
}

上述代码不会输出"load child";因此通过子类来引用父类中定义的静态字段,不会触发子类的初始化。

  • 通过数组定义来引用类,不会触发此类的初始化
public static void main(String[] args) {
    Father[] fathers = new Father[2];
}

上述代码不会输出"load father";说明没有触发Father类的初始化。

  • 引用常量不会触发此类的初始化
public class Constant {
    static {
        System.out.println("load Constant");
    }
    public static final String HELLO = "hello";
}
public class TestDemo {
    public static void main(String[] args) {
        System.out.println(Constant.HELLO);
    }
}

上述代码不会输出"load Constant";这是因为虽然在Constant中定义了常量HELLO,但其实在编译阶段通过常量传播优化,已经将此常量的值"hello"存储到了TestDemo类的常量池中,以后TestDemo对常量Constant.HELLO的引用实际被转化为TestDemo类对自身常量池的引用了。也就是说,实际上TestDemo的Class文件之中并没有Constant类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。

接口的初始化

接口也有初始化过程,上面类的初始化是通过静态代码块"static{}"来输出初始化信息的,而接口中不能使用"static{}",但编译器任然会为接口生成"()"类构造器,用于初始化接口中所定义的成员变量。接口与类真正的区别是前面5种初始化场景中的第3种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父类全部都完成了初始化,只有在真正使用到父类接口的时候才会初始化。

你可能感兴趣的:(java)