虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校检、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
1 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存,整个生命周期为下图所示:
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定(它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。
对于类的初始化虚拟机规范中明确指出有且只有一下五中情况会立即执行对类的初始化:
1、遇到 new、getstatic、putstatic或invokestatic 着四条指令时,这四条指令分别代表: 使用new关键字实例化对象、读取或设置静态字段(被final修饰,编译器把结果已经放入常量池的静态字段除外)、调用一个类的静态方法。 2、使用jav.lang.reflect包中的方法对类进行反射调用。 3、当初始化一个类的时候,如果该类的父类没有初始化,则需要先初始化起父类 4、当虚拟机启动时,用户需要指定一个要执行的主类(包括main()方法的那个类),虚拟机会先初始化这个主类 5、使用jdk1.7的动态语言支持时,如果一个java.lang.MethodHandle实例最后的解析结果是:
REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个方法
句柄所对应的类没有初始化则需要先触发其初始化
对于五种触发类进行初始化的场景,虚拟机规范中使用了一个强烈的限定语:“有且只有”,这五种场景称为对一个类进行主动引用。
除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
什么是被动引用呢?
例子1
对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段
只会触发父类的初始化而不会触发子类的初始化。因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发
子类的初始化
//通过子类引用父类的静态字段,不会导致子类初始化
package org.fenixsoft.classloading;
class SuperClass{ static{ System.out.println("SuperClass init!"); } public static int value=123; } class SubClass extends SuperClass{ static{ System.out.println("SubClass init!"); } } public class NotInitialization{ public static void main(String[] args){ System.out.println(SubClass.value); } }
只会输出SuperClass.init.
例子2:
//通过数组定义引用类,不会触发此类的初始化 package org.fenixsoft.classloading; class SuperClass{ static{ System.out.println("SuperClass init!"); } public static int value=123; } class SubClass extends SuperClass{ static{ System.out.println("SubClass init!"); } } public class NotInitialization{ public static void main(String[] args){ SuperClass[] sca=new SuperClass[10]; } }
没有输出SuperClass. init;说明没有触发类
org.fenixsoft.classloading.SuperClass的初始化阶段,但是触发了另外一个名为“[Lorg.fenixsoft.classloading.SuperClass"的类的初始化阶段
这并不是一个合法的类名称,他是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作有字节码指令newarray触发。这个类代表了类类型的一维数组
数组中该有的属性和方法都实现在这个类里
例子3
//常量的引用不会出发定义常亮的类的初始化 package org.fenixsoft.classloading; class ConstClass{ static{ System.out.println("ConstClass init!"); } public static final String HELLO="know"; } public class NoitInitialization1 { public static void main(String[] args){ System.out.println(ConstClass.HELLO); } }
输出结果只有know,虽然引用的是ConstClass类中的常量,但是在编译器通过常量传播优化,已经将常量的值存储到了NotInitialization1中的常量池中了,以后
NotInitialization1对常量ConstClass.HELLO的引用都转化成NotInitialization1类对自身常量池的引用,也就是说:NotInitialization1的Class文件中没有ConstClass类的符号引用,编译以后这俩兄弟没有一点关系了。
接口的加载过程和类加载过程有一些不同:接口也有初始化过程,上面的代码使用静态语句块static{}来输出初始化信息,接口中不能使用静态块,但编译器仍然回味接口生成“<clinit>()”类构造器,用于初始化接口中所定义的成员变量。接口与类的真正区别是接口的初始化时,不需要负借口全部完成初始化,只有在真正使用到付接口的时候才会初始化